mirror of
https://github.com/grafana/grafana.git
synced 2024-12-28 01:41:24 -06:00
AuthN: Add session client (#60894)
* add basic session client * populate UserToken in ReqContext * token rotation as a post auth hook * fixed in context handler * add session token rotation * add session token tests * use namespacedID constructor
This commit is contained in:
parent
ebb34560a4
commit
a226903ec6
@ -9,13 +9,16 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"github.com/grafana/grafana/pkg/web"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ClientAPIKey = "auth.client.api-key" // #nosec G101
|
ClientAPIKey = "auth.client.api-key" // #nosec G101
|
||||||
|
ClientSession = "auth.client.session"
|
||||||
ClientAnonymous = "auth.client.anonymous"
|
ClientAnonymous = "auth.client.anonymous"
|
||||||
ClientBasic = "auth.client.basic"
|
ClientBasic = "auth.client.basic"
|
||||||
ClientRender = "auth.client.render"
|
ClientRender = "auth.client.render"
|
||||||
@ -27,7 +30,8 @@ type ClientParams struct {
|
|||||||
EnableDisabledUsers bool
|
EnableDisabledUsers bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostAuthHookFn func(ctx context.Context, clientParams *ClientParams, identity *Identity) error
|
type PostAuthHookFn func(ctx context.Context,
|
||||||
|
clientParams *ClientParams, identity *Identity, r *Request) error
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
// RegisterPostAuthHook registers a hook that is called after a successful authentication.
|
// RegisterPostAuthHook registers a hook that is called after a successful authentication.
|
||||||
@ -48,6 +52,9 @@ type Request struct {
|
|||||||
// OrgID will be populated by authn.Service
|
// OrgID will be populated by authn.Service
|
||||||
OrgID int64
|
OrgID int64
|
||||||
HTTPRequest *http.Request
|
HTTPRequest *http.Request
|
||||||
|
|
||||||
|
// for use in post auth hooks
|
||||||
|
Resp web.ResponseWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -69,12 +76,14 @@ type Identity struct {
|
|||||||
IsGrafanaAdmin *bool
|
IsGrafanaAdmin *bool
|
||||||
AuthModule string // AuthModule is the name of the external system
|
AuthModule string // AuthModule is the name of the external system
|
||||||
AuthID string // AuthId is the unique identifier for the user in the external system
|
AuthID string // AuthId is the unique identifier for the user in the external system
|
||||||
OAuthToken *oauth2.Token
|
|
||||||
LookUpParams models.UserLookupParams
|
LookUpParams models.UserLookupParams
|
||||||
IsDisabled bool
|
IsDisabled bool
|
||||||
HelpFlags1 user.HelpFlags1
|
HelpFlags1 user.HelpFlags1
|
||||||
LastSeenAt time.Time
|
LastSeenAt time.Time
|
||||||
Teams []int64
|
Teams []int64
|
||||||
|
|
||||||
|
OAuthToken *oauth2.Token
|
||||||
|
SessionToken *auth.UserToken
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Identity) Role() org.RoleType {
|
func (i *Identity) Role() org.RoleType {
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/apikey"
|
"github.com/grafana/grafana/pkg/services/apikey"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
sync "github.com/grafana/grafana/pkg/services/authn/authnimpl/usersync"
|
sync "github.com/grafana/grafana/pkg/services/authn/authnimpl/usersync"
|
||||||
"github.com/grafana/grafana/pkg/services/authn/clients"
|
"github.com/grafana/grafana/pkg/services/authn/clients"
|
||||||
@ -26,8 +27,11 @@ import (
|
|||||||
var _ authn.Service = new(Service)
|
var _ authn.Service = new(Service)
|
||||||
|
|
||||||
func ProvideService(
|
func ProvideService(
|
||||||
cfg *setting.Cfg, tracer tracing.Tracer, orgService org.Service, accessControlService accesscontrol.Service,
|
cfg *setting.Cfg, tracer tracing.Tracer,
|
||||||
apikeyService apikey.Service, userService user.Service, loginAttempts loginattempt.Service, quotaService quota.Service,
|
orgService org.Service, sessionService auth.UserTokenService,
|
||||||
|
accessControlService accesscontrol.Service,
|
||||||
|
apikeyService apikey.Service, userService user.Service,
|
||||||
|
loginAttempts loginattempt.Service, quotaService quota.Service,
|
||||||
authInfoService login.AuthInfoService, renderService rendering.Service,
|
authInfoService login.AuthInfoService, renderService rendering.Service,
|
||||||
) *Service {
|
) *Service {
|
||||||
s := &Service{
|
s := &Service{
|
||||||
@ -41,6 +45,10 @@ func ProvideService(
|
|||||||
s.clients[authn.ClientRender] = clients.ProvideRender(userService, renderService)
|
s.clients[authn.ClientRender] = clients.ProvideRender(userService, renderService)
|
||||||
s.clients[authn.ClientAPIKey] = clients.ProvideAPIKey(apikeyService, userService)
|
s.clients[authn.ClientAPIKey] = clients.ProvideAPIKey(apikeyService, userService)
|
||||||
|
|
||||||
|
sessionClient := clients.ProvideSession(sessionService, userService, cfg.LoginCookieName, cfg.LoginMaxLifetime)
|
||||||
|
s.clients[authn.ClientSession] = sessionClient
|
||||||
|
s.RegisterPostAuthHook(sessionClient.RefreshTokenHook)
|
||||||
|
|
||||||
if s.cfg.AnonymousEnabled {
|
if s.cfg.AnonymousEnabled {
|
||||||
s.clients[authn.ClientAnonymous] = clients.ProvideAnonymous(cfg, orgService)
|
s.clients[authn.ClientAnonymous] = clients.ProvideAnonymous(cfg, orgService)
|
||||||
}
|
}
|
||||||
@ -107,7 +115,7 @@ func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Requ
|
|||||||
params := c.ClientParams()
|
params := c.ClientParams()
|
||||||
|
|
||||||
for _, hook := range s.postAuthHooks {
|
for _, hook := range s.postAuthHooks {
|
||||||
if err := hook(ctx, params, identity); err != nil {
|
if err := hook(ctx, params, identity, r); err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,8 @@ type OrgSync struct {
|
|||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OrgSync) SyncOrgUser(ctx context.Context, clientParams *authn.ClientParams, id *authn.Identity) error {
|
func (s *OrgSync) SyncOrgUser(ctx context.Context,
|
||||||
|
clientParams *authn.ClientParams, id *authn.Identity, _ *authn.Request) error {
|
||||||
if !clientParams.SyncUser {
|
if !clientParams.SyncUser {
|
||||||
s.log.Debug("Not syncing org user", "auth_module", id.AuthModule, "auth_id", id.AuthID)
|
s.log.Debug("Not syncing org user", "auth_module", id.AuthModule, "auth_id", id.AuthID)
|
||||||
return nil
|
return nil
|
||||||
|
@ -113,7 +113,7 @@ func TestOrgSync_SyncOrgUser(t *testing.T) {
|
|||||||
accessControl: tt.fields.accessControl,
|
accessControl: tt.fields.accessControl,
|
||||||
log: tt.fields.log,
|
log: tt.fields.log,
|
||||||
}
|
}
|
||||||
if err := s.SyncOrgUser(tt.args.ctx, tt.args.clientParams, tt.args.id); (err != nil) != tt.wantErr {
|
if err := s.SyncOrgUser(tt.args.ctx, tt.args.clientParams, tt.args.id, nil); (err != nil) != tt.wantErr {
|
||||||
t.Errorf("OrgSync.SyncOrgUser() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("OrgSync.SyncOrgUser() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,8 @@ type UserSync struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SyncUser syncs a user with the database
|
// SyncUser syncs a user with the database
|
||||||
func (s *UserSync) SyncUser(ctx context.Context, clientParams *authn.ClientParams, id *authn.Identity) error {
|
func (s *UserSync) SyncUser(ctx context.Context,
|
||||||
|
clientParams *authn.ClientParams, id *authn.Identity, _ *authn.Request) error {
|
||||||
if !clientParams.SyncUser {
|
if !clientParams.SyncUser {
|
||||||
s.log.Debug("Not syncing user", "auth_module", id.AuthModule, "auth_id", id.AuthID)
|
s.log.Debug("Not syncing user", "auth_module", id.AuthModule, "auth_id", id.AuthID)
|
||||||
return nil
|
return nil
|
||||||
|
@ -430,7 +430,7 @@ func TestUserSync_SyncUser(t *testing.T) {
|
|||||||
quotaService: tt.fields.quotaService,
|
quotaService: tt.fields.quotaService,
|
||||||
log: tt.fields.log,
|
log: tt.fields.log,
|
||||||
}
|
}
|
||||||
err := s.SyncUser(tt.args.ctx, tt.args.clientParams, tt.args.id)
|
err := s.SyncUser(tt.args.ctx, tt.args.clientParams, tt.args.id, nil)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
return
|
return
|
||||||
|
129
pkg/services/authn/clients/session.go
Normal file
129
pkg/services/authn/clients/session.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package clients
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/network"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware/cookies"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"github.com/grafana/grafana/pkg/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ authn.Client = new(Session)
|
||||||
|
|
||||||
|
func ProvideSession(sessionService auth.UserTokenService, userService user.Service,
|
||||||
|
cookieName string, maxLifetime time.Duration) *Session {
|
||||||
|
return &Session{
|
||||||
|
loginCookieName: cookieName,
|
||||||
|
loginMaxLifetime: maxLifetime,
|
||||||
|
sessionService: sessionService,
|
||||||
|
userService: userService,
|
||||||
|
log: log.New(authn.ClientSession),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
loginCookieName string
|
||||||
|
loginMaxLifetime time.Duration // jguer: should be returned by session Service on rotate
|
||||||
|
sessionService auth.UserTokenService
|
||||||
|
userService user.Service
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) ClientParams() *authn.ClientParams {
|
||||||
|
return &authn.ClientParams{
|
||||||
|
SyncUser: false,
|
||||||
|
AllowSignUp: false,
|
||||||
|
EnableDisabledUsers: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Test(ctx context.Context, r *authn.Request) bool {
|
||||||
|
if s.loginCookieName == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.HTTPRequest.Cookie(s.loginCookieName); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
|
unescapedCookie, err := r.HTTPRequest.Cookie(s.loginCookieName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSessionToken, err := url.QueryUnescape(unescapedCookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := s.sessionService.LookupToken(ctx, rawSessionToken)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("failed to look up session from cookie", "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
signedInUser, err := s.userService.GetSignedInUserWithCacheCtx(ctx,
|
||||||
|
&user.GetSignedInUserQuery{UserID: token.UserId, OrgID: r.OrgID})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("failed to get user with id", "userId", token.UserId, "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME (jguer): oauth token refresh not implemented
|
||||||
|
identity := authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser)
|
||||||
|
identity.SessionToken = token
|
||||||
|
|
||||||
|
return identity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) RefreshTokenHook(ctx context.Context,
|
||||||
|
clientParams *authn.ClientParams, identity *authn.Identity, r *authn.Request) error {
|
||||||
|
if identity.SessionToken == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Resp.Before(func(w web.ResponseWriter) {
|
||||||
|
if w.Written() || errors.Is(ctx.Err(), context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME (jguer): get real values
|
||||||
|
addr := web.RemoteAddr(r.HTTPRequest)
|
||||||
|
userAgent := r.HTTPRequest.UserAgent()
|
||||||
|
|
||||||
|
// addr := reqContext.RemoteAddr()
|
||||||
|
ip, err := network.GetIPFromAddress(addr)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Debug("failed to get client IP address", "addr", addr, "err", err)
|
||||||
|
ip = nil
|
||||||
|
}
|
||||||
|
rotated, err := s.sessionService.TryRotateToken(ctx, identity.SessionToken, ip, userAgent)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("failed to rotate token", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rotated {
|
||||||
|
s.log.Debug("rotated session token", "user", identity.ID)
|
||||||
|
|
||||||
|
maxAge := int(s.loginMaxLifetime.Seconds())
|
||||||
|
if s.loginMaxLifetime <= 0 {
|
||||||
|
maxAge = -1
|
||||||
|
}
|
||||||
|
cookies.WriteCookie(r.Resp, s.loginCookieName, url.QueryEscape(identity.SessionToken.UnhashedToken), maxAge, nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
191
pkg/services/authn/clients/session_test.go
Normal file
191
pkg/services/authn/clients/session_test.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package clients
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/models/roletype"
|
||||||
|
"github.com/grafana/grafana/pkg/models/usertoken"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth/authtest"
|
||||||
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||||
|
"github.com/grafana/grafana/pkg/web"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSession_ClientParams(t *testing.T) {
|
||||||
|
s := ProvideSession(nil, nil, "", 0)
|
||||||
|
require.Equal(t, &authn.ClientParams{
|
||||||
|
SyncUser: false,
|
||||||
|
AllowSignUp: false,
|
||||||
|
EnableDisabledUsers: false,
|
||||||
|
}, s.ClientParams())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSession_Test(t *testing.T) {
|
||||||
|
cookieName := "grafana_session"
|
||||||
|
|
||||||
|
validHTTPReq := &http.Request{
|
||||||
|
Header: map[string][]string{},
|
||||||
|
}
|
||||||
|
validHTTPReq.AddCookie(&http.Cookie{Name: cookieName, Value: "bob-the-high-entropy-token"})
|
||||||
|
|
||||||
|
s := ProvideSession(&authtest.FakeUserAuthTokenService{}, &usertest.FakeUserService{}, "", 20*time.Second)
|
||||||
|
|
||||||
|
disabled := s.Test(context.Background(), &authn.Request{HTTPRequest: validHTTPReq})
|
||||||
|
assert.False(t, disabled)
|
||||||
|
|
||||||
|
s.loginCookieName = cookieName
|
||||||
|
|
||||||
|
good := s.Test(context.Background(), &authn.Request{HTTPRequest: validHTTPReq})
|
||||||
|
assert.True(t, good)
|
||||||
|
|
||||||
|
invalidHTTPReq := &http.Request{Header: map[string][]string{}}
|
||||||
|
|
||||||
|
bad := s.Test(context.Background(), &authn.Request{HTTPRequest: invalidHTTPReq})
|
||||||
|
assert.False(t, bad)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSession_Authenticate(t *testing.T) {
|
||||||
|
cookieName := "grafana_session"
|
||||||
|
|
||||||
|
validHTTPReq := &http.Request{
|
||||||
|
Header: map[string][]string{},
|
||||||
|
}
|
||||||
|
validHTTPReq.AddCookie(&http.Cookie{Name: cookieName, Value: "bob-the-high-entropy-token"})
|
||||||
|
|
||||||
|
sampleToken := &usertoken.UserToken{
|
||||||
|
Id: 1,
|
||||||
|
UserId: 1,
|
||||||
|
AuthToken: "hashyToken",
|
||||||
|
PrevAuthToken: "prevHashyToken",
|
||||||
|
AuthTokenSeen: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleUser := &user.SignedInUser{
|
||||||
|
UserID: 1,
|
||||||
|
Name: "sample user",
|
||||||
|
Login: "sample_user",
|
||||||
|
Email: "sample_user@samples.iwz",
|
||||||
|
OrgID: 1,
|
||||||
|
OrgRole: roletype.RoleEditor,
|
||||||
|
}
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
sessionService auth.UserTokenService
|
||||||
|
userService user.Service
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
r *authn.Request
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantID *authn.Identity
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cookie not found",
|
||||||
|
fields: fields{sessionService: &authtest.FakeUserAuthTokenService{}, userService: &usertest.FakeUserService{}},
|
||||||
|
args: args{r: &authn.Request{HTTPRequest: &http.Request{}}},
|
||||||
|
wantID: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
fields: fields{sessionService: &authtest.FakeUserAuthTokenService{LookupTokenProvider: func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) {
|
||||||
|
return sampleToken, nil
|
||||||
|
}}, userService: &usertest.FakeUserService{ExpectedSignedInUser: sampleUser}},
|
||||||
|
args: args{r: &authn.Request{HTTPRequest: validHTTPReq}},
|
||||||
|
wantID: &authn.Identity{
|
||||||
|
SessionToken: sampleToken,
|
||||||
|
ID: "user:1",
|
||||||
|
Name: "sample user",
|
||||||
|
Login: "sample_user",
|
||||||
|
Email: "sample_user@samples.iwz",
|
||||||
|
OrgID: 1,
|
||||||
|
OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleEditor},
|
||||||
|
LookUpParams: models.UserLookupParams{},
|
||||||
|
IsGrafanaAdmin: boolPtr(false),
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := ProvideSession(tt.fields.sessionService, tt.fields.userService, cookieName, 20*time.Second)
|
||||||
|
|
||||||
|
got, err := s.Authenticate(context.Background(), tt.args.r)
|
||||||
|
require.True(t, (err != nil) == tt.wantErr, err)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.EqualValues(t, tt.wantID, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeResponseWriter struct {
|
||||||
|
Status int
|
||||||
|
HeaderStore http.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResponseWriter) Header() http.Header {
|
||||||
|
return f.HeaderStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResponseWriter) Write([]byte) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
f.Status = statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSession_RefreshHook(t *testing.T) {
|
||||||
|
s := ProvideSession(&authtest.FakeUserAuthTokenService{
|
||||||
|
TryRotateTokenProvider: func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, error) {
|
||||||
|
token.UnhashedToken = "new-token"
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
}, &usertest.FakeUserService{}, "grafana-session", 20*time.Second)
|
||||||
|
|
||||||
|
sampleID := &authn.Identity{
|
||||||
|
SessionToken: &auth.UserToken{
|
||||||
|
Id: 1,
|
||||||
|
UserId: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockResponseWriter := &fakeResponseWriter{
|
||||||
|
Status: 0,
|
||||||
|
HeaderStore: map[string][]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &authn.Request{
|
||||||
|
HTTPRequest: &http.Request{
|
||||||
|
Header: map[string][]string{},
|
||||||
|
},
|
||||||
|
Resp: web.NewResponseWriter(http.MethodConnect, mockResponseWriter),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.RefreshTokenHook(context.Background(), &authn.ClientParams{}, sampleID, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resp.Resp.WriteHeader(201)
|
||||||
|
require.Equal(t, 201, mockResponseWriter.Status)
|
||||||
|
|
||||||
|
assert.Equal(t, "new-token", sampleID.SessionToken.UnhashedToken)
|
||||||
|
require.Len(t, mockResponseWriter.HeaderStore, 1)
|
||||||
|
assert.Equal(t, "grafana-session=new-token; Path=/; Max-Age=20; HttpOnly",
|
||||||
|
mockResponseWriter.HeaderStore.Get("set-cookie"), mockResponseWriter.HeaderStore)
|
||||||
|
}
|
@ -476,6 +476,29 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ContextHandler) initContextWithToken(reqContext *models.ReqContext, orgID int64) bool {
|
func (h *ContextHandler) initContextWithToken(reqContext *models.ReqContext, orgID int64) bool {
|
||||||
|
if h.features.IsEnabled(featuremgmt.FlagAuthnService) {
|
||||||
|
identity, ok, err := h.authnService.Authenticate(reqContext.Req.Context(),
|
||||||
|
authn.ClientSession, &authn.Request{HTTPRequest: reqContext.Req, Resp: reqContext.Resp})
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, auth.ErrUserTokenNotFound) || errors.Is(err, auth.ErrInvalidSessionToken) {
|
||||||
|
// Burn the cookie in case of invalid, expired or missing token
|
||||||
|
reqContext.Resp.Before(h.deleteInvalidCookieEndOfRequestFunc(reqContext))
|
||||||
|
}
|
||||||
|
|
||||||
|
writeErr(reqContext, err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
reqContext.IsSignedIn = true
|
||||||
|
reqContext.SignedInUser = identity.SignedInUser()
|
||||||
|
reqContext.UserToken = identity.SessionToken
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if h.Cfg.LoginCookieName == "" {
|
if h.Cfg.LoginCookieName == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -586,6 +609,8 @@ func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *models.ReqContext) w
|
|||||||
reqContext.Logger.Debug("Failed to get client IP address", "addr", addr, "err", err)
|
reqContext.Logger.Debug("Failed to get client IP address", "addr", addr, "err", err)
|
||||||
ip = nil
|
ip = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME (jguer): rotation should return a new token instead of modifying the existing one.
|
||||||
rotated, err := h.AuthTokenService.TryRotateToken(ctx, reqContext.UserToken, ip, reqContext.Req.UserAgent())
|
rotated, err := h.AuthTokenService.TryRotateToken(ctx, reqContext.UserToken, ip, reqContext.Req.UserAgent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reqContext.Logger.Error("Failed to rotate token", "error", err)
|
reqContext.Logger.Error("Failed to rotate token", "error", err)
|
||||||
|
@ -62,12 +62,16 @@ func (ctx *Context) run() {
|
|||||||
|
|
||||||
// RemoteAddr returns more real IP address.
|
// RemoteAddr returns more real IP address.
|
||||||
func (ctx *Context) RemoteAddr() string {
|
func (ctx *Context) RemoteAddr() string {
|
||||||
addr := ctx.Req.Header.Get("X-Real-IP")
|
return RemoteAddr(ctx.Req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoteAddr(req *http.Request) string {
|
||||||
|
addr := req.Header.Get("X-Real-IP")
|
||||||
|
|
||||||
if len(addr) == 0 {
|
if len(addr) == 0 {
|
||||||
// X-Forwarded-For may contain multiple IP addresses, separated by
|
// X-Forwarded-For may contain multiple IP addresses, separated by
|
||||||
// commas.
|
// commas.
|
||||||
addr = strings.TrimSpace(strings.Split(ctx.Req.Header.Get("X-Forwarded-For"), ",")[0])
|
addr = strings.TrimSpace(strings.Split(req.Header.Get("X-Forwarded-For"), ",")[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse user inputs from headers to prevent log forgery
|
// parse user inputs from headers to prevent log forgery
|
||||||
@ -79,7 +83,7 @@ func (ctx *Context) RemoteAddr() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(addr) == 0 {
|
if len(addr) == 0 {
|
||||||
addr = ctx.Req.RemoteAddr
|
addr = req.RemoteAddr
|
||||||
if i := strings.LastIndex(addr, ":"); i > -1 {
|
if i := strings.LastIndex(addr, ":"); i > -1 {
|
||||||
addr = addr[:i]
|
addr = addr[:i]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user