OAuth: Use the attached external session data in OAuthToken and OAuthTokenSync (#96655)
* wip * wip + tests * wip * wip opt2 * Use authn.Identity struct's SessionToken * Merge fixes * Handle disabling the feature flag correctly * Fix test * Cleanup * Remove HasOAuthEntry from the OAuthTokenService interface * Remove unused function
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
glog "github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -57,7 +58,8 @@ type httpClient interface {
|
||||
func NewDataSourceProxy(ds *datasources.DataSource, pluginRoutes []*plugins.Route, ctx *contextmodel.ReqContext,
|
||||
proxyPath string, cfg *setting.Cfg, clientProvider httpclient.Provider,
|
||||
oAuthTokenService oauthtoken.OAuthTokenService, dsService datasources.DataSourceService,
|
||||
tracer tracing.Tracer, features featuremgmt.FeatureToggles) (*DataSourceProxy, error) {
|
||||
tracer tracing.Tracer, features featuremgmt.FeatureToggles,
|
||||
) (*DataSourceProxy, error) {
|
||||
targetURL, err := datasource.ValidateURL(ds.Type, ds.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -261,7 +263,8 @@ func (proxy *DataSourceProxy) director(req *http.Request) {
|
||||
}
|
||||
|
||||
if proxy.oAuthTokenService.IsOAuthPassThruEnabled(proxy.ds) {
|
||||
if token := proxy.oAuthTokenService.GetCurrentOAuthToken(req.Context(), proxy.ctx.SignedInUser); token != nil {
|
||||
reqCtx := contexthandler.FromContext(req.Context())
|
||||
if token := proxy.oAuthTokenService.GetCurrentOAuthToken(req.Context(), proxy.ctx.SignedInUser, reqCtx.UserToken); token != nil {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken))
|
||||
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
|
||||
@@ -32,7 +32,9 @@ import (
|
||||
pluginfakes "github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||
@@ -557,7 +559,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
var routes []*plugins.Route
|
||||
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/path/to/folder/", func(proxy *DataSourceProxy) {
|
||||
proxy.oAuthTokenService = &oauthtokentest.MockOauthTokenService{
|
||||
GetCurrentOauthTokenFunc: func(_ context.Context, _ identity.Requester) *oauth2.Token {
|
||||
GetCurrentOauthTokenFunc: func(_ context.Context, _ identity.Requester, _ *auth.UserToken) *oauth2.Token {
|
||||
return (&oauth2.Token{
|
||||
AccessToken: "testtoken",
|
||||
RefreshToken: "testrefreshtoken",
|
||||
@@ -573,6 +575,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
req = req.WithContext(context.WithValue(req.Context(), ctxkey.Key{}, &contextmodel.ReqContext{UserToken: nil}))
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.director(req)
|
||||
|
||||
@@ -74,12 +74,15 @@ type CreateTokenCommand struct {
|
||||
}
|
||||
|
||||
// UserTokenService are used for generating and validating user tokens
|
||||
//
|
||||
//go:generate mockery --name UserTokenService --structname MockUserAuthTokenService --outpkg authtest --filename auth_token_service_mock.go --output ./authtest/
|
||||
type UserTokenService interface {
|
||||
CreateToken(ctx context.Context, cmd *CreateTokenCommand) (*UserToken, error)
|
||||
LookupToken(ctx context.Context, unhashedToken string) (*UserToken, error)
|
||||
GetTokenByExternalSessionID(ctx context.Context, externalSessionID int64) (*UserToken, error)
|
||||
GetExternalSession(ctx context.Context, extSessionID int64) (*ExternalSession, error)
|
||||
GetExternalSession(ctx context.Context, externalSessionID int64) (*ExternalSession, error)
|
||||
FindExternalSessions(ctx context.Context, query *ListExternalSessionQuery) ([]*ExternalSession, error)
|
||||
UpdateExternalSession(ctx context.Context, externalSessionID int64, cmd *UpdateExternalSessionCommand) error
|
||||
|
||||
// RotateToken will always rotate a valid token
|
||||
RotateToken(ctx context.Context, cmd RotateCommand) (*UserToken, error)
|
||||
|
||||
@@ -248,14 +248,18 @@ func (s *UserAuthTokenService) GetTokenByExternalSessionID(ctx context.Context,
|
||||
return &userToken, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) GetExternalSession(ctx context.Context, extSessionID int64) (*auth.ExternalSession, error) {
|
||||
return s.externalSessionStore.Get(ctx, extSessionID)
|
||||
func (s *UserAuthTokenService) GetExternalSession(ctx context.Context, externalSessionID int64) (*auth.ExternalSession, error) {
|
||||
return s.externalSessionStore.Get(ctx, externalSessionID)
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) FindExternalSessions(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error) {
|
||||
return s.externalSessionStore.List(ctx, query)
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) UpdateExternalSession(ctx context.Context, externalSessionID int64, cmd *auth.UpdateExternalSessionCommand) error {
|
||||
return s.externalSessionStore.Update(ctx, externalSessionID, cmd)
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateCommand) (*auth.UserToken, error) {
|
||||
if cmd.UnHashedToken == "" {
|
||||
return nil, auth.ErrInvalidSessionToken
|
||||
|
||||
@@ -27,11 +27,11 @@ func provideExternalSessionStore(sqlStore db.DB, secretService secrets.Service,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *store) Get(ctx context.Context, extSessionID int64) (*auth.ExternalSession, error) {
|
||||
func (s *store) Get(ctx context.Context, ID int64) (*auth.ExternalSession, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "externalsession.Get")
|
||||
defer span.End()
|
||||
|
||||
externalSession := &auth.ExternalSession{ID: extSessionID}
|
||||
externalSession := &auth.ExternalSession{ID: ID}
|
||||
|
||||
err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
found, err := sess.Get(externalSession)
|
||||
@@ -150,6 +150,45 @@ func (s *store) Create(ctx context.Context, extSession *auth.ExternalSession) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) Update(ctx context.Context, ID int64, cmd *auth.UpdateExternalSessionCommand) error {
|
||||
ctx, span := s.tracer.Start(ctx, "externalsession.Update")
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
externalSession := &auth.ExternalSession{}
|
||||
|
||||
externalSession.AccessToken, err = s.encryptAndEncode(cmd.Token.AccessToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
externalSession.RefreshToken, err = s.encryptAndEncode(cmd.Token.RefreshToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var secretIdToken string
|
||||
if idToken, ok := cmd.Token.Extra("id_token").(string); ok && idToken != "" {
|
||||
secretIdToken, err = s.encryptAndEncode(idToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
externalSession.IDToken = secretIdToken
|
||||
}
|
||||
|
||||
externalSession.ExpiresAt = cmd.Token.Expiry
|
||||
|
||||
err = s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
_, err := sess.ID(ID).Cols("access_token", "refresh_token", "id_token", "expires_at").Update(externalSession)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) Delete(ctx context.Context, ID int64) error {
|
||||
ctx, span := s.tracer.Start(ctx, "externalsession.Delete")
|
||||
defer span.End()
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
// Code generated by mockery v2.42.1. DO NOT EDIT.
|
||||
|
||||
package authtest
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
auth "github.com/grafana/grafana/pkg/services/auth"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
usertoken "github.com/grafana/grafana/pkg/models/usertoken"
|
||||
)
|
||||
|
||||
// MockUserAuthTokenService is an autogenerated mock type for the UserTokenService type
|
||||
type MockUserAuthTokenService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ActiveTokenCount provides a mock function with given fields: ctx, userID
|
||||
func (_m *MockUserAuthTokenService) ActiveTokenCount(ctx context.Context, userID *int64) (int64, error) {
|
||||
ret := _m.Called(ctx, userID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ActiveTokenCount")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *int64) (int64, error)); ok {
|
||||
return rf(ctx, userID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *int64) int64); ok {
|
||||
r0 = rf(ctx, userID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok {
|
||||
r1 = rf(ctx, userID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CreateToken provides a mock function with given fields: ctx, cmd
|
||||
func (_m *MockUserAuthTokenService) CreateToken(ctx context.Context, cmd *auth.CreateTokenCommand) (*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CreateToken")
|
||||
}
|
||||
|
||||
var r0 *usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *auth.CreateTokenCommand) (*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, cmd)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *auth.CreateTokenCommand) *usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, cmd)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *auth.CreateTokenCommand) error); ok {
|
||||
r1 = rf(ctx, cmd)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// FindExternalSessions provides a mock function with given fields: ctx, query
|
||||
func (_m *MockUserAuthTokenService) FindExternalSessions(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for FindExternalSessions")
|
||||
}
|
||||
|
||||
var r0 []*auth.ExternalSession
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error)); ok {
|
||||
return rf(ctx, query)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *auth.ListExternalSessionQuery) []*auth.ExternalSession); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*auth.ExternalSession)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *auth.ListExternalSessionQuery) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetExternalSession provides a mock function with given fields: ctx, externalSessionID
|
||||
func (_m *MockUserAuthTokenService) GetExternalSession(ctx context.Context, externalSessionID int64) (*auth.ExternalSession, error) {
|
||||
ret := _m.Called(ctx, externalSessionID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetExternalSession")
|
||||
}
|
||||
|
||||
var r0 *auth.ExternalSession
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (*auth.ExternalSession, error)); ok {
|
||||
return rf(ctx, externalSessionID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *auth.ExternalSession); ok {
|
||||
r0 = rf(ctx, externalSessionID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*auth.ExternalSession)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, externalSessionID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTokenByExternalSessionID provides a mock function with given fields: ctx, externalSessionID
|
||||
func (_m *MockUserAuthTokenService) GetTokenByExternalSessionID(ctx context.Context, externalSessionID int64) (*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, externalSessionID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetTokenByExternalSessionID")
|
||||
}
|
||||
|
||||
var r0 *usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, externalSessionID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, externalSessionID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, externalSessionID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUserRevokedTokens provides a mock function with given fields: ctx, userID
|
||||
func (_m *MockUserAuthTokenService) GetUserRevokedTokens(ctx context.Context, userID int64) ([]*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, userID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetUserRevokedTokens")
|
||||
}
|
||||
|
||||
var r0 []*usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) ([]*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, userID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) []*usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, userID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, userID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUserToken provides a mock function with given fields: ctx, userID, userTokenID
|
||||
func (_m *MockUserAuthTokenService) GetUserToken(ctx context.Context, userID int64, userTokenID int64) (*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, userID, userTokenID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetUserToken")
|
||||
}
|
||||
|
||||
var r0 *usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, userID, userTokenID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, userID, userTokenID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64, int64) error); ok {
|
||||
r1 = rf(ctx, userID, userTokenID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUserTokens provides a mock function with given fields: ctx, userID
|
||||
func (_m *MockUserAuthTokenService) GetUserTokens(ctx context.Context, userID int64) ([]*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, userID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetUserTokens")
|
||||
}
|
||||
|
||||
var r0 []*usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) ([]*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, userID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) []*usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, userID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, userID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LookupToken provides a mock function with given fields: ctx, unhashedToken
|
||||
func (_m *MockUserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, unhashedToken)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for LookupToken")
|
||||
}
|
||||
|
||||
var r0 *usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, unhashedToken)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, unhashedToken)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, unhashedToken)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RevokeAllUserTokens provides a mock function with given fields: ctx, userID
|
||||
func (_m *MockUserAuthTokenService) RevokeAllUserTokens(ctx context.Context, userID int64) error {
|
||||
ret := _m.Called(ctx, userID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RevokeAllUserTokens")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, userID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RevokeToken provides a mock function with given fields: ctx, token, soft
|
||||
func (_m *MockUserAuthTokenService) RevokeToken(ctx context.Context, token *usertoken.UserToken, soft bool) error {
|
||||
ret := _m.Called(ctx, token, soft)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RevokeToken")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *usertoken.UserToken, bool) error); ok {
|
||||
r0 = rf(ctx, token, soft)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RotateToken provides a mock function with given fields: ctx, cmd
|
||||
func (_m *MockUserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateCommand) (*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RotateToken")
|
||||
}
|
||||
|
||||
var r0 *usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, auth.RotateCommand) (*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, cmd)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, auth.RotateCommand) *usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, cmd)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, auth.RotateCommand) error); ok {
|
||||
r1 = rf(ctx, cmd)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UpdateExternalSession provides a mock function with given fields: ctx, externalSessionID, cmd
|
||||
func (_m *MockUserAuthTokenService) UpdateExternalSession(ctx context.Context, externalSessionID int64, cmd *auth.UpdateExternalSessionCommand) error {
|
||||
ret := _m.Called(ctx, externalSessionID, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateExternalSession")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, *auth.UpdateExternalSessionCommand) error); ok {
|
||||
r0 = rf(ctx, externalSessionID, cmd)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewMockUserAuthTokenService creates a new instance of MockUserAuthTokenService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockUserAuthTokenService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockUserAuthTokenService {
|
||||
mock := &MockUserAuthTokenService{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -147,6 +147,24 @@ func (_m *MockExternalSessionStore) List(ctx context.Context, query *auth.ListEx
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, ID, cmd
|
||||
func (_m *MockExternalSessionStore) Update(ctx context.Context, ID int64, cmd *auth.UpdateExternalSessionCommand) error {
|
||||
ret := _m.Called(ctx, ID, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, *auth.UpdateExternalSessionCommand) error); ok {
|
||||
r0 = rf(ctx, ID, cmd)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewMockExternalSessionStore creates a new instance of MockExternalSessionStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockExternalSessionStore(t interface {
|
||||
|
||||
@@ -14,12 +14,15 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
)
|
||||
|
||||
var _ auth.UserTokenService = (*FakeUserAuthTokenService)(nil)
|
||||
|
||||
type FakeUserAuthTokenService struct {
|
||||
CreateTokenProvider func(ctx context.Context, cmd *auth.CreateTokenCommand) (*auth.UserToken, error)
|
||||
RotateTokenProvider func(ctx context.Context, cmd auth.RotateCommand) (*auth.UserToken, error)
|
||||
GetTokenByExternalSessionIDProvider func(ctx context.Context, externalSessionID int64) (*auth.UserToken, error)
|
||||
GetExternalSessionProvider func(ctx context.Context, externalSessionID int64) (*auth.ExternalSession, error)
|
||||
FindExternalSessionsProvider func(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error)
|
||||
UpdateExternalSessionProvider func(ctx context.Context, externalSessionID int64, cmd *auth.UpdateExternalSessionCommand) error
|
||||
TryRotateTokenProvider func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, *auth.UserToken, error)
|
||||
LookupTokenProvider func(ctx context.Context, unhashedToken string) (*auth.UserToken, error)
|
||||
RevokeTokenProvider func(ctx context.Context, token *auth.UserToken, soft bool) error
|
||||
@@ -98,6 +101,10 @@ func (s *FakeUserAuthTokenService) FindExternalSessions(ctx context.Context, que
|
||||
return s.FindExternalSessionsProvider(context.Background(), query)
|
||||
}
|
||||
|
||||
func (s *FakeUserAuthTokenService) UpdateExternalSession(ctx context.Context, externalSessionID int64, cmd *auth.UpdateExternalSessionCommand) error {
|
||||
return s.UpdateExternalSessionProvider(context.Background(), externalSessionID, cmd)
|
||||
}
|
||||
|
||||
func (s *FakeUserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*auth.UserToken, error) {
|
||||
return s.LookupTokenProvider(context.Background(), unhashedToken)
|
||||
}
|
||||
@@ -149,16 +156,6 @@ func (ts *FakeOAuthTokenService) IsOAuthPassThruEnabled(*datasources.DataSource)
|
||||
return ts.passThruEnabled
|
||||
}
|
||||
|
||||
func (ts *FakeOAuthTokenService) HasOAuthEntry(context.Context, identity.Requester) (*login.UserAuth, bool, error) {
|
||||
if ts.ExpectedAuthUser != nil {
|
||||
return ts.ExpectedAuthUser, true, nil
|
||||
}
|
||||
if error, ok := ts.ExpectedErrors["HasOAuthEntry"]; ok {
|
||||
return nil, false, error
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (ts *FakeOAuthTokenService) InvalidateOAuthTokens(ctx context.Context, usr *login.UserAuth) error {
|
||||
ts.ExpectedAuthUser.OAuthAccessToken = ""
|
||||
ts.ExpectedAuthUser.OAuthRefreshToken = ""
|
||||
|
||||
@@ -3,6 +3,8 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type ExternalSession struct {
|
||||
@@ -43,6 +45,10 @@ func (e *ExternalSession) Clone() *ExternalSession {
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateExternalSessionCommand struct {
|
||||
Token *oauth2.Token
|
||||
}
|
||||
|
||||
type ListExternalSessionQuery struct {
|
||||
ID int64
|
||||
NameID string
|
||||
@@ -57,6 +63,8 @@ type ExternalSessionStore interface {
|
||||
List(ctx context.Context, query *ListExternalSessionQuery) ([]*ExternalSession, error)
|
||||
// Create creates a new external session for a user
|
||||
Create(ctx context.Context, extSesion *ExternalSession) error
|
||||
// Update updates an external session
|
||||
Update(ctx context.Context, ID int64, cmd *UpdateExternalSessionCommand) error
|
||||
// Delete deletes an external session
|
||||
Delete(ctx context.Context, ID int64) error
|
||||
// DeleteExternalSessionsByUserID deletes an external session
|
||||
|
||||
@@ -178,7 +178,7 @@ type RedirectClient interface {
|
||||
// that should happen during logout and supports client specific redirect URL.
|
||||
type LogoutClient interface {
|
||||
Client
|
||||
Logout(ctx context.Context, user identity.Requester) (*Redirect, bool)
|
||||
Logout(ctx context.Context, user identity.Requester, sessionToken *usertoken.UserToken) (*Redirect, bool)
|
||||
}
|
||||
|
||||
type SSOSettingsAwareClient interface {
|
||||
|
||||
@@ -37,7 +37,7 @@ func ProvideRegistration(
|
||||
jwtService auth.JWTVerifierService, userProtectionService login.UserProtectionService,
|
||||
loginAttempts loginattempt.Service, quotaService quota.Service,
|
||||
authInfoService login.AuthInfoService, renderService rendering.Service,
|
||||
features *featuremgmt.FeatureManager, oauthTokenService oauthtoken.OAuthTokenService,
|
||||
features featuremgmt.FeatureToggles, oauthTokenService oauthtoken.OAuthTokenService,
|
||||
socialService social.Service, cache *remotecache.RemoteCache,
|
||||
ldapService service.LDAP, settingsProviderService setting.Provider,
|
||||
tracer tracing.Tracer, tempUserService tempuser.Service, notificationService notifications.Service,
|
||||
@@ -108,13 +108,13 @@ func ProvideRegistration(
|
||||
}
|
||||
|
||||
// FIXME (jguer): move to User package
|
||||
userSync := sync.ProvideUserSync(userService, userProtectionService, authInfoService, quotaService, tracer)
|
||||
userSync := sync.ProvideUserSync(userService, userProtectionService, authInfoService, quotaService, tracer, features)
|
||||
orgSync := sync.ProvideOrgSync(userService, orgService, accessControlService, cfg, tracer)
|
||||
authnSvc.RegisterPostAuthHook(userSync.SyncUserHook, 10)
|
||||
authnSvc.RegisterPostAuthHook(userSync.EnableUserHook, 20)
|
||||
authnSvc.RegisterPostAuthHook(orgSync.SyncOrgRolesHook, 30)
|
||||
authnSvc.RegisterPostAuthHook(userSync.SyncLastSeenHook, 130)
|
||||
authnSvc.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService, tracer).SyncOauthTokenHook, 60)
|
||||
authnSvc.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService, tracer, features).SyncOauthTokenHook, 60)
|
||||
authnSvc.RegisterPostAuthHook(userSync.FetchSyncedUserHook, 100)
|
||||
|
||||
rbacSync := sync.ProvideRBACSync(accessControlService, tracer, permRegistry)
|
||||
|
||||
@@ -322,7 +322,7 @@ func (s *Service) Logout(ctx context.Context, user identity.Requester, sessionTo
|
||||
goto Default
|
||||
}
|
||||
|
||||
clientRedirect, ok := logoutClient.Logout(ctx, user)
|
||||
clientRedirect, ok := logoutClient.Logout(ctx, user, sessionToken)
|
||||
if !ok {
|
||||
goto Default
|
||||
}
|
||||
|
||||
@@ -507,7 +507,7 @@ func TestService_Logout(t *testing.T) {
|
||||
expectedRedirect: &authn.Redirect{URL: "http://idp.com/logout"},
|
||||
client: &authntest.MockClient{
|
||||
NameFunc: func() string { return "auth.client.azuread" },
|
||||
LogoutFunc: func(ctx context.Context, _ identity.Requester) (*authn.Redirect, bool) {
|
||||
LogoutFunc: func(ctx context.Context, _ identity.Requester, sessionToken *usertoken.UserToken) (*authn.Redirect, bool) {
|
||||
return &authn.Redirect{URL: "http://idp.com/logout"}, true
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,12 +17,15 @@ import (
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken"
|
||||
)
|
||||
|
||||
const maxOAuthTokenCacheTTL = 5 * time.Minute
|
||||
|
||||
func ProvideOAuthTokenSync(service oauthtoken.OAuthTokenService, sessionService auth.UserTokenService, socialService social.Service, tracer tracing.Tracer) *OAuthTokenSync {
|
||||
func ProvideOAuthTokenSync(service oauthtoken.OAuthTokenService, sessionService auth.UserTokenService, socialService social.Service, tracer tracing.Tracer,
|
||||
features featuremgmt.FeatureToggles,
|
||||
) *OAuthTokenSync {
|
||||
return &OAuthTokenSync{
|
||||
log.New("oauth_token.sync"),
|
||||
service,
|
||||
@@ -31,6 +34,7 @@ func ProvideOAuthTokenSync(service oauthtoken.OAuthTokenService, sessionService
|
||||
new(singleflight.Group),
|
||||
tracer,
|
||||
localcache.New(maxOAuthTokenCacheTTL, 15*time.Minute),
|
||||
features,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +46,7 @@ type OAuthTokenSync struct {
|
||||
singleflightGroup *singleflight.Group
|
||||
tracer tracing.Tracer
|
||||
cache *localcache.CacheService
|
||||
features featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error {
|
||||
@@ -72,6 +77,10 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, id *authn.Ident
|
||||
ctxLogger := s.log.FromContext(ctx).New("userID", userID)
|
||||
|
||||
cacheKey := fmt.Sprintf("token-check-%s", id.GetID())
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
cacheKey = fmt.Sprintf("token-check-%s-%d", id.GetID(), id.SessionToken.Id)
|
||||
}
|
||||
|
||||
if _, ok := s.cache.Get(cacheKey); ok {
|
||||
ctxLogger.Debug("Expiration check has been cached, no need to refresh")
|
||||
return nil
|
||||
@@ -83,7 +92,7 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, id *authn.Ident
|
||||
updateCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
token, refreshErr := s.service.TryTokenRefresh(updateCtx, id)
|
||||
token, refreshErr := s.service.TryTokenRefresh(updateCtx, id, id.SessionToken)
|
||||
if refreshErr != nil {
|
||||
if errors.Is(refreshErr, context.Canceled) {
|
||||
return nil, nil
|
||||
|
||||
@@ -20,6 +20,9 @@ import (
|
||||
"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/contexthandler/ctxkey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest"
|
||||
)
|
||||
@@ -85,7 +88,7 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) {
|
||||
)
|
||||
|
||||
service := &oauthtokentest.MockOauthTokenService{
|
||||
TryTokenRefreshFunc: func(ctx context.Context, usr identity.Requester) (*oauth2.Token, error) {
|
||||
TryTokenRefreshFunc: func(ctx context.Context, usr identity.Requester, _ *auth.UserToken) (*oauth2.Token, error) {
|
||||
tryRefreshCalled = true
|
||||
return nil, tt.expectedTryRefreshErr
|
||||
},
|
||||
@@ -116,9 +119,13 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) {
|
||||
singleflightGroup: new(singleflight.Group),
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
cache: localcache.New(maxOAuthTokenCacheTTL, 15*time.Minute),
|
||||
features: featuremgmt.WithFeatures(),
|
||||
}
|
||||
|
||||
err := sync.SyncOauthTokenHook(context.Background(), tt.identity, nil)
|
||||
ctx := context.Background()
|
||||
reqCtx := context.WithValue(ctx, ctxkey.Key{}, &contextmodel.ReqContext{UserToken: nil})
|
||||
|
||||
err := sync.SyncOauthTokenHook(reqCtx, tt.identity, nil)
|
||||
assert.ErrorIs(t, err, tt.expectedErr)
|
||||
assert.Equal(t, tt.expectTryRefreshTokenCalled, tryRefreshCalled)
|
||||
assert.Equal(t, tt.expectRevokeTokenCalled, revokeTokenCalled)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
@@ -51,7 +52,9 @@ var (
|
||||
errSignupNotAllowed = errors.New("system administrator has disabled signup")
|
||||
)
|
||||
|
||||
func ProvideUserSync(userService user.Service, userProtectionService login.UserProtectionService, authInfoService login.AuthInfoService, quotaService quota.Service, tracer tracing.Tracer) *UserSync {
|
||||
func ProvideUserSync(userService user.Service, userProtectionService login.UserProtectionService, authInfoService login.AuthInfoService,
|
||||
quotaService quota.Service, tracer tracing.Tracer, features featuremgmt.FeatureToggles,
|
||||
) *UserSync {
|
||||
return &UserSync{
|
||||
userService: userService,
|
||||
authInfoService: authInfoService,
|
||||
@@ -59,6 +62,7 @@ func ProvideUserSync(userService user.Service, userProtectionService login.UserP
|
||||
quotaService: quotaService,
|
||||
log: log.New("user.sync"),
|
||||
tracer: tracer,
|
||||
features: features,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +73,7 @@ type UserSync struct {
|
||||
quotaService quota.Service
|
||||
log log.Logger
|
||||
tracer tracing.Tracer
|
||||
features featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
// SyncUserHook syncs a user with the database
|
||||
@@ -223,21 +228,30 @@ func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, ident
|
||||
// This can happen when: using multiple auth client where the same user exists in several or
|
||||
// changing to new auth client
|
||||
if createConnection {
|
||||
return s.authInfoService.SetAuthInfo(ctx, &login.SetAuthInfoCommand{
|
||||
setAuthInfoCmd := &login.SetAuthInfoCommand{
|
||||
UserId: userID,
|
||||
AuthModule: identity.AuthenticatedBy,
|
||||
AuthId: identity.AuthID,
|
||||
OAuthToken: identity.OAuthToken,
|
||||
})
|
||||
}
|
||||
|
||||
if !s.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
setAuthInfoCmd.OAuthToken = identity.OAuthToken
|
||||
}
|
||||
return s.authInfoService.SetAuthInfo(ctx, setAuthInfoCmd)
|
||||
}
|
||||
|
||||
s.log.FromContext(ctx).Debug("Updating auth connection for user", "id", identity.ID)
|
||||
return s.authInfoService.UpdateAuthInfo(ctx, &login.UpdateAuthInfoCommand{
|
||||
updateAuthInfoCmd := &login.UpdateAuthInfoCommand{
|
||||
UserId: userID,
|
||||
AuthId: identity.AuthID,
|
||||
AuthModule: identity.AuthenticatedBy,
|
||||
OAuthToken: identity.OAuthToken,
|
||||
})
|
||||
}
|
||||
|
||||
if !s.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
updateAuthInfoCmd.OAuthToken = identity.OAuthToken
|
||||
}
|
||||
|
||||
s.log.FromContext(ctx).Debug("Updating auth connection for user", "id", identity.ID)
|
||||
return s.authInfoService.UpdateAuthInfo(ctx, updateAuthInfoCmd)
|
||||
}
|
||||
|
||||
func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id *authn.Identity, userAuth *login.UserAuth) error {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfoimpl"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfotest"
|
||||
@@ -45,7 +46,9 @@ func TestUserSync_SyncUserHook(t *testing.T) {
|
||||
AuthModule: "oauth",
|
||||
AuthId: "2032",
|
||||
UserId: 1,
|
||||
Id: 1}}
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
|
||||
userService := &usertest.FakeUserService{ExpectedUser: &user.User{
|
||||
ID: 1,
|
||||
@@ -434,7 +437,7 @@ func TestUserSync_SyncUserHook(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := ProvideUserSync(tt.fields.userService, userProtection, tt.fields.authInfoService, tt.fields.quotaService, tracing.InitializeTracerForTest())
|
||||
s := ProvideUserSync(tt.fields.userService, userProtection, tt.fields.authInfoService, tt.fields.quotaService, tracing.InitializeTracerForTest(), featuremgmt.WithFeatures())
|
||||
err := s.SyncUserHook(tt.args.ctx, tt.args.id, nil)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
|
||||
@@ -85,7 +85,7 @@ type MockClient struct {
|
||||
TestFunc func(ctx context.Context, r *authn.Request) bool
|
||||
PriorityFunc func() uint
|
||||
HookFunc func(ctx context.Context, identity *authn.Identity, r *authn.Request) error
|
||||
LogoutFunc func(ctx context.Context, user identity.Requester) (*authn.Redirect, bool)
|
||||
LogoutFunc func(ctx context.Context, user identity.Requester, sessionToken *usertoken.UserToken) (*authn.Redirect, bool)
|
||||
IdentityTypeFunc func() claims.IdentityType
|
||||
ResolveIdentityFunc func(ctx context.Context, orgID int64, typ claims.IdentityType, id string) (*authn.Identity, error)
|
||||
}
|
||||
@@ -133,9 +133,9 @@ func (m MockClient) Hook(ctx context.Context, identity *authn.Identity, r *authn
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockClient) Logout(ctx context.Context, user identity.Requester) (*authn.Redirect, bool) {
|
||||
func (m *MockClient) Logout(ctx context.Context, user identity.Requester, sessionToken *usertoken.UserToken) (*authn.Redirect, bool) {
|
||||
if m.LogoutFunc != nil {
|
||||
return m.LogoutFunc(ctx, user)
|
||||
return m.LogoutFunc(ctx, user, sessionToken)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/login/social/connectors"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
@@ -257,8 +258,8 @@ func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redir
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *OAuth) Logout(ctx context.Context, user identity.Requester) (*authn.Redirect, bool) {
|
||||
token := c.oauthService.GetCurrentOAuthToken(ctx, user)
|
||||
func (c *OAuth) Logout(ctx context.Context, user identity.Requester, sessionToken *auth.UserToken) (*authn.Redirect, bool) {
|
||||
token := c.oauthService.GetCurrentOAuthToken(ctx, user, sessionToken)
|
||||
|
||||
userID, err := identity.UserIdentifier(user.GetID())
|
||||
if err != nil {
|
||||
@@ -268,7 +269,7 @@ func (c *OAuth) Logout(ctx context.Context, user identity.Requester) (*authn.Red
|
||||
|
||||
ctxLogger := c.log.FromContext(ctx).New("userID", userID)
|
||||
|
||||
if err := c.oauthService.InvalidateOAuthTokens(ctx, user); err != nil {
|
||||
if err := c.oauthService.InvalidateOAuthTokens(ctx, user, sessionToken); err != nil {
|
||||
ctxLogger.Error("Failed to invalidate tokens", "error", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/login/social/socialtest"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
@@ -469,7 +470,7 @@ func TestOAuth_Logout(t *testing.T) {
|
||||
)
|
||||
|
||||
mockService := &oauthtokentest.MockOauthTokenService{
|
||||
GetCurrentOauthTokenFunc: func(_ context.Context, _ identity.Requester) *oauth2.Token {
|
||||
GetCurrentOauthTokenFunc: func(_ context.Context, _ identity.Requester, _ *auth.UserToken) *oauth2.Token {
|
||||
getTokenCalled = true
|
||||
token := &oauth2.Token{
|
||||
AccessToken: "some.access.token",
|
||||
@@ -479,7 +480,7 @@ func TestOAuth_Logout(t *testing.T) {
|
||||
"id_token": "some.id.token",
|
||||
})
|
||||
},
|
||||
InvalidateOAuthTokensFunc: func(_ context.Context, _ identity.Requester) error {
|
||||
InvalidateOAuthTokensFunc: func(_ context.Context, _ identity.Requester, _ *auth.UserToken) error {
|
||||
invalidateTokenCalled = true
|
||||
return nil
|
||||
},
|
||||
@@ -490,7 +491,7 @@ func TestOAuth_Logout(t *testing.T) {
|
||||
}
|
||||
c := ProvideOAuth(authn.ClientWithPrefix("azuread"), tt.cfg, mockService, fakeSocialSvc, &setting.OSSImpl{Cfg: tt.cfg}, featuremgmt.WithFeatures())
|
||||
|
||||
redirect, ok := c.Logout(context.Background(), &authn.Identity{ID: "1", Type: claims.TypeUser})
|
||||
redirect, ok := c.Logout(context.Background(), &authn.Identity{ID: "1", Type: claims.TypeUser}, nil)
|
||||
|
||||
assert.Equal(t, tt.expectedOK, ok)
|
||||
if tt.expectedOK {
|
||||
|
||||
@@ -92,7 +92,6 @@ func (s *Store) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuer
|
||||
err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.Table("user_auth").In("user_id", params).OrderBy("created").Find(&userAuths)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -187,7 +186,9 @@ func (s *Store) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCom
|
||||
}
|
||||
|
||||
return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
upd, err := sess.MustCols("o_auth_expiry").Where("user_id = ? AND auth_module = ?", cmd.UserId, cmd.AuthModule).Update(authUser)
|
||||
upd, err := sess.MustCols("o_auth_expiry", "o_auth_access_token", "o_auth_refresh_token", "o_auth_id_token", "o_auth_token_type").
|
||||
Where("user_id = ? AND auth_module = ?", cmd.UserId, cmd.AuthModule).
|
||||
Update(authUser)
|
||||
|
||||
s.logger.Debug("Updated user_auth", "user_id", cmd.UserId, "auth_id", cmd.AuthId, "auth_module", cmd.AuthModule, "rows", upd)
|
||||
|
||||
@@ -198,7 +199,6 @@ func (s *Store) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCom
|
||||
"SELECT id FROM user_auth WHERE user_id = ? AND auth_module = ? AND auth_id = ?",
|
||||
cmd.UserId, cmd.AuthModule, cmd.AuthId,
|
||||
).Get(&id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -220,7 +220,7 @@ func (s *Store) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCom
|
||||
|
||||
func (s *Store) DeleteUserAuthInfo(ctx context.Context, userID int64) error {
|
||||
return s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
var rawSQL = "DELETE FROM user_auth WHERE user_id = ?"
|
||||
rawSQL := "DELETE FROM user_auth WHERE user_id = ?"
|
||||
_, err := sess.Exec(rawSQL, userID)
|
||||
return err
|
||||
})
|
||||
|
||||
@@ -19,7 +19,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -39,28 +41,33 @@ type Service struct {
|
||||
Cfg *setting.Cfg
|
||||
SocialService social.Service
|
||||
AuthInfoService login.AuthInfoService
|
||||
sessionService auth.UserTokenService
|
||||
features featuremgmt.FeatureToggles
|
||||
serverLock *serverlock.ServerLockService
|
||||
tracer tracing.Tracer
|
||||
|
||||
tokenRefreshDuration *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
var _ OAuthTokenService = (*Service)(nil)
|
||||
|
||||
//go:generate mockery --name OAuthTokenService --structname MockService --outpkg oauthtokentest --filename service_mock.go --output ./oauthtokentest/
|
||||
type OAuthTokenService interface {
|
||||
GetCurrentOAuthToken(context.Context, identity.Requester) *oauth2.Token
|
||||
GetCurrentOAuthToken(context.Context, identity.Requester, *auth.UserToken) *oauth2.Token
|
||||
IsOAuthPassThruEnabled(*datasources.DataSource) bool
|
||||
HasOAuthEntry(context.Context, identity.Requester) (*login.UserAuth, bool, error)
|
||||
TryTokenRefresh(context.Context, identity.Requester) (*oauth2.Token, error)
|
||||
InvalidateOAuthTokens(context.Context, identity.Requester) error
|
||||
TryTokenRefresh(context.Context, identity.Requester, *auth.UserToken) (*oauth2.Token, error)
|
||||
InvalidateOAuthTokens(context.Context, identity.Requester, *auth.UserToken) error
|
||||
}
|
||||
|
||||
func ProvideService(socialService social.Service, authInfoService login.AuthInfoService, cfg *setting.Cfg, registerer prometheus.Registerer,
|
||||
serverLockService *serverlock.ServerLockService, tracer tracing.Tracer,
|
||||
serverLockService *serverlock.ServerLockService, tracer tracing.Tracer, sessionService auth.UserTokenService, features featuremgmt.FeatureToggles,
|
||||
) *Service {
|
||||
return &Service{
|
||||
AuthInfoService: authInfoService,
|
||||
sessionService: sessionService,
|
||||
Cfg: cfg,
|
||||
SocialService: socialService,
|
||||
features: features,
|
||||
serverLock: serverLockService,
|
||||
tokenRefreshDuration: newTokenRefreshDurationMetric(registerer),
|
||||
tracer: tracer,
|
||||
@@ -68,7 +75,7 @@ func ProvideService(socialService social.Service, authInfoService login.AuthInfo
|
||||
}
|
||||
|
||||
// GetCurrentOAuthToken returns the OAuth token, if any, for the authenticated user. Will try to refresh the token if it has expired.
|
||||
func (o *Service) GetCurrentOAuthToken(ctx context.Context, usr identity.Requester) *oauth2.Token {
|
||||
func (o *Service) GetCurrentOAuthToken(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) *oauth2.Token {
|
||||
ctx, span := o.tracer.Start(ctx, "oauthtoken.GetCurrentOAuthToken")
|
||||
defer span.End()
|
||||
|
||||
@@ -93,30 +100,54 @@ func (o *Service) GetCurrentOAuthToken(ctx context.Context, usr identity.Request
|
||||
|
||||
ctxLogger = ctxLogger.New("userID", userID)
|
||||
|
||||
authInfo, ok, _ := o.HasOAuthEntry(ctx, usr)
|
||||
if !ok {
|
||||
if !strings.HasPrefix(usr.GetAuthenticatedBy(), "oauth_") {
|
||||
ctxLogger.Warn("The specified user's auth provider is not oauth",
|
||||
"authmodule", usr.GetAuthenticatedBy())
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := checkOAuthRefreshToken(authInfo); err != nil {
|
||||
if errors.Is(err, ErrNoRefreshTokenFound) {
|
||||
return buildOAuthTokenFromAuthInfo(authInfo)
|
||||
var persistedToken *oauth2.Token
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
externalSession, err := o.sessionService.GetExternalSession(ctx, sessionToken.ExternalSessionId)
|
||||
if err != nil {
|
||||
if errors.Is(err, auth.ErrExternalSessionNotFound) {
|
||||
return nil
|
||||
}
|
||||
ctxLogger.Error("Failed to fetch external session", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
persistedToken = buildOAuthTokenFromExternalSession(externalSession)
|
||||
|
||||
persistedToken := buildOAuthTokenFromAuthInfo(authInfo)
|
||||
if persistedToken.RefreshToken == "" {
|
||||
return persistedToken
|
||||
}
|
||||
} else {
|
||||
authInfo, ok, _ := o.hasOAuthEntry(ctx, usr)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := checkOAuthRefreshToken(authInfo); err != nil {
|
||||
if errors.Is(err, ErrNoRefreshTokenFound) {
|
||||
return buildOAuthTokenFromAuthInfo(authInfo)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
persistedToken = buildOAuthTokenFromAuthInfo(authInfo)
|
||||
}
|
||||
|
||||
refreshNeeded := needTokenRefresh(ctx, persistedToken)
|
||||
if !refreshNeeded {
|
||||
return persistedToken
|
||||
}
|
||||
|
||||
token, err := o.TryTokenRefresh(ctx, usr)
|
||||
token, err := o.TryTokenRefresh(ctx, usr, sessionToken)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNoRefreshTokenFound) {
|
||||
return buildOAuthTokenFromAuthInfo(authInfo)
|
||||
return persistedToken
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -130,9 +161,9 @@ func (o *Service) IsOAuthPassThruEnabled(ds *datasources.DataSource) bool {
|
||||
return IsOAuthPassThruEnabled(ds)
|
||||
}
|
||||
|
||||
// HasOAuthEntry returns true and the UserAuth object when OAuth info exists for the specified User
|
||||
func (o *Service) HasOAuthEntry(ctx context.Context, usr identity.Requester) (*login.UserAuth, bool, error) {
|
||||
ctx, span := o.tracer.Start(ctx, "oauthtoken.HasOAuthEntry")
|
||||
// hasOAuthEntry returns true and the UserAuth object when OAuth info exists for the specified User
|
||||
func (o *Service) hasOAuthEntry(ctx context.Context, usr identity.Requester) (*login.UserAuth, bool, error) {
|
||||
ctx, span := o.tracer.Start(ctx, "oauthtoken.hasOAuthEntry")
|
||||
defer span.End()
|
||||
|
||||
if usr == nil || usr.IsNil() {
|
||||
@@ -167,12 +198,19 @@ func (o *Service) HasOAuthEntry(ctx context.Context, usr identity.Requester) (*l
|
||||
if !strings.Contains(authInfo.AuthModule, "oauth") {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// An extra check to ensure that the user has an OAuth token
|
||||
// It's required to handle the case when the `improvedExternalSessionHandling` feature flag gets disabled
|
||||
if authInfo.OAuthAccessToken == "" {
|
||||
ctxLogger.Debug("No access token found for user")
|
||||
return nil, false, fmt.Errorf("no access token found for user %d", userID)
|
||||
}
|
||||
return authInfo, true, nil
|
||||
}
|
||||
|
||||
// TryTokenRefresh returns an error in case the OAuth token refresh was unsuccessful
|
||||
// It uses a server lock to prevent getting the Refresh Token multiple times for a given User
|
||||
func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester) (*oauth2.Token, error) {
|
||||
func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) (*oauth2.Token, error) {
|
||||
ctx, span := o.tracer.Start(ctx, "oauthtoken.TryTokenRefresh")
|
||||
defer span.End()
|
||||
|
||||
@@ -218,6 +256,9 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester) (
|
||||
}
|
||||
|
||||
lockKey := fmt.Sprintf("oauth-refresh-token-%d", userID)
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
lockKey = fmt.Sprintf("oauth-refresh-token-%d-%d", userID, sessionToken.ExternalSessionId)
|
||||
}
|
||||
|
||||
lockTimeConfig := serverlock.LockTimeConfig{
|
||||
MaxInterval: 30 * time.Second,
|
||||
@@ -242,15 +283,32 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester) (
|
||||
|
||||
ctxLogger.Debug("Serverlock request for getting a new access token", "key", lockKey)
|
||||
|
||||
authInfo, exists, err := o.HasOAuthEntry(ctx, usr)
|
||||
if !exists {
|
||||
var persistedToken *oauth2.Token
|
||||
var externalSession *auth.ExternalSession
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
externalSession, err = o.sessionService.GetExternalSession(ctx, sessionToken.ExternalSessionId)
|
||||
if err != nil {
|
||||
ctxLogger.Debug("Failed to fetch oauth entry", "error", err)
|
||||
if errors.Is(err, auth.ErrExternalSessionNotFound) {
|
||||
ctxLogger.Error("External session was not found for user", "error", err)
|
||||
return
|
||||
}
|
||||
ctxLogger.Error("Failed to fetch external session", "error", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
persistedToken := buildOAuthTokenFromAuthInfo(authInfo)
|
||||
persistedToken = buildOAuthTokenFromExternalSession(externalSession)
|
||||
} else {
|
||||
authInfo, exists, err := o.hasOAuthEntry(ctx, usr)
|
||||
if !exists {
|
||||
if err != nil {
|
||||
ctxLogger.Debug("Failed to fetch oauth entry", "error", err)
|
||||
cmdErr = err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
persistedToken = buildOAuthTokenFromAuthInfo(authInfo)
|
||||
}
|
||||
|
||||
needRefresh := needTokenRefresh(ctx, persistedToken)
|
||||
if !needRefresh {
|
||||
@@ -259,7 +317,7 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester) (
|
||||
return
|
||||
}
|
||||
|
||||
newToken, cmdErr = o.tryGetOrRefreshOAuthToken(ctx, persistedToken, usr)
|
||||
newToken, cmdErr = o.tryGetOrRefreshOAuthToken(ctx, persistedToken, usr, sessionToken)
|
||||
}, retryOpt)
|
||||
if lockErr != nil {
|
||||
ctxLogger.Error("Failed to obtain token refresh lock", "error", lockErr)
|
||||
@@ -274,45 +332,27 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester) (
|
||||
return newToken, cmdErr
|
||||
}
|
||||
|
||||
func buildOAuthTokenFromAuthInfo(authInfo *login.UserAuth) *oauth2.Token {
|
||||
token := &oauth2.Token{
|
||||
AccessToken: authInfo.OAuthAccessToken,
|
||||
Expiry: authInfo.OAuthExpiry,
|
||||
RefreshToken: authInfo.OAuthRefreshToken,
|
||||
TokenType: authInfo.OAuthTokenType,
|
||||
}
|
||||
|
||||
if authInfo.OAuthIdToken != "" {
|
||||
token = token.WithExtra(map[string]any{"id_token": authInfo.OAuthIdToken})
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func checkOAuthRefreshToken(authInfo *login.UserAuth) error {
|
||||
if !strings.Contains(authInfo.AuthModule, "oauth") {
|
||||
logger.Warn("The specified user's auth provider is not oauth",
|
||||
"authmodule", authInfo.AuthModule, "userid", authInfo.UserId)
|
||||
return ErrNotAnOAuthProvider
|
||||
}
|
||||
|
||||
if authInfo.OAuthRefreshToken == "" {
|
||||
logger.Warn("No refresh token available",
|
||||
"authmodule", authInfo.AuthModule, "userid", authInfo.UserId)
|
||||
return ErrNoRefreshTokenFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InvalidateOAuthTokens invalidates the OAuth tokens (access_token, refresh_token) and sets the Expiry to default/zero
|
||||
func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr identity.Requester) error {
|
||||
func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) error {
|
||||
userID, err := usr.GetInternalID()
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert user id to int", "id", usr.GetID(), "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
ctxLogger := logger.FromContext(ctx).New("userID", userID)
|
||||
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
err := o.sessionService.UpdateExternalSession(ctx, sessionToken.ExternalSessionId, &auth.UpdateExternalSessionCommand{
|
||||
Token: &oauth2.Token{},
|
||||
})
|
||||
if err != nil {
|
||||
ctxLogger.Error("Failed to update external session", "error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Should this run regardless of the feature flag?
|
||||
return o.AuthInfoService.UpdateAuthInfo(ctx, &login.UpdateAuthInfoCommand{
|
||||
UserId: userID,
|
||||
AuthModule: usr.GetAuthenticatedBy(),
|
||||
@@ -325,7 +365,7 @@ func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr identity.Reques
|
||||
})
|
||||
}
|
||||
|
||||
func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken *oauth2.Token, usr identity.Requester) (*oauth2.Token, error) {
|
||||
func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken *oauth2.Token, usr identity.Requester, sessionToken *auth.UserToken) (*oauth2.Token, error) {
|
||||
ctx, span := o.tracer.Start(ctx, "oauthtoken.tryGetOrRefreshOAuthToken")
|
||||
defer span.End()
|
||||
|
||||
@@ -374,7 +414,7 @@ func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken
|
||||
"provider", usr.GetAuthenticatedBy(), "error", err)
|
||||
|
||||
// token refresh failed, invalidate the old token
|
||||
if err := o.InvalidateOAuthTokens(ctx, usr); err != nil {
|
||||
if err := o.InvalidateOAuthTokens(ctx, usr, sessionToken); err != nil {
|
||||
ctxLogger.Warn("Failed to invalidate OAuth tokens", "authID", usr.GetAuthID(), "error", err)
|
||||
}
|
||||
|
||||
@@ -399,10 +439,20 @@ func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken
|
||||
)
|
||||
}
|
||||
|
||||
if err := o.AuthInfoService.UpdateAuthInfo(ctx, updateAuthCommand); err != nil {
|
||||
ctxLogger.Error("Failed to update auth info during token refresh", "authID", usr.GetAuthID(), "error", err)
|
||||
return token, err
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
if err := o.sessionService.UpdateExternalSession(ctx, sessionToken.ExternalSessionId, &auth.UpdateExternalSessionCommand{
|
||||
Token: token,
|
||||
}); err != nil {
|
||||
ctxLogger.Error("Failed to update external session during token refresh", "error", err)
|
||||
return token, err
|
||||
}
|
||||
} else {
|
||||
if err := o.AuthInfoService.UpdateAuthInfo(ctx, updateAuthCommand); err != nil {
|
||||
ctxLogger.Error("Failed to update auth info during token refresh", "authID", usr.GetAuthID(), "error", err)
|
||||
return token, err
|
||||
}
|
||||
}
|
||||
|
||||
ctxLogger.Debug("Updated oauth info for user")
|
||||
}
|
||||
|
||||
@@ -467,6 +517,51 @@ func needTokenRefresh(ctx context.Context, persistedToken *oauth2.Token) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func buildOAuthTokenFromAuthInfo(authInfo *login.UserAuth) *oauth2.Token {
|
||||
token := &oauth2.Token{
|
||||
AccessToken: authInfo.OAuthAccessToken,
|
||||
Expiry: authInfo.OAuthExpiry,
|
||||
RefreshToken: authInfo.OAuthRefreshToken,
|
||||
TokenType: authInfo.OAuthTokenType,
|
||||
}
|
||||
|
||||
if authInfo.OAuthIdToken != "" {
|
||||
token = token.WithExtra(map[string]any{"id_token": authInfo.OAuthIdToken})
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func buildOAuthTokenFromExternalSession(externalSession *auth.ExternalSession) *oauth2.Token {
|
||||
token := &oauth2.Token{
|
||||
AccessToken: externalSession.AccessToken,
|
||||
Expiry: externalSession.ExpiresAt,
|
||||
RefreshToken: externalSession.RefreshToken,
|
||||
}
|
||||
|
||||
if externalSession.IDToken != "" {
|
||||
token = token.WithExtra(map[string]any{"id_token": externalSession.IDToken})
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func checkOAuthRefreshToken(authInfo *login.UserAuth) error {
|
||||
if !strings.Contains(authInfo.AuthModule, "oauth") {
|
||||
logger.Warn("The specified user's auth provider is not oauth",
|
||||
"authmodule", authInfo.AuthModule, "userid", authInfo.UserId)
|
||||
return ErrNotAnOAuthProvider
|
||||
}
|
||||
|
||||
if authInfo.OAuthRefreshToken == "" {
|
||||
logger.Warn("No refresh token available",
|
||||
"authmodule", authInfo.AuthModule, "userid", authInfo.UserId)
|
||||
return ErrNoRefreshTokenFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIDTokenExpiry extracts the expiry time from the ID token
|
||||
func GetIDTokenExpiry(token *oauth2.Token) (time.Time, error) {
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
|
||||
@@ -3,32 +3,29 @@ package oauthtoken
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/login/social/socialtest"
|
||||
"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/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfoimpl"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfotest"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const EXPIRED_ID_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6InlvdXItY2xpZW50LWlkIiwiZXhwIjoxNjAwMDAwMDAwLCJpYXQiOjE2MDAwMDAwMDAsIm5hbWUiOiJKb2huIERvZSIsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSJ9.c2lnbmF0dXJl" // #nosec G101 not a hardcoded credential
|
||||
@@ -39,104 +36,6 @@ func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
func TestService_HasOAuthEntry(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *user.SignedInUser
|
||||
want *login.UserAuth
|
||||
wantExist bool
|
||||
wantErr bool
|
||||
err error
|
||||
getAuthInfoErr error
|
||||
getAuthInfoUser login.UserAuth
|
||||
}{
|
||||
{
|
||||
name: "returns false without an error in case user is nil",
|
||||
user: nil,
|
||||
want: nil,
|
||||
wantExist: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "returns false and an error in case GetAuthInfo returns an error",
|
||||
user: &user.SignedInUser{UserID: 1},
|
||||
want: nil,
|
||||
wantExist: false,
|
||||
wantErr: true,
|
||||
getAuthInfoErr: errors.New("error"),
|
||||
},
|
||||
{
|
||||
name: "returns false without an error in case auth entry is not found",
|
||||
user: &user.SignedInUser{UserID: 1},
|
||||
want: nil,
|
||||
wantExist: false,
|
||||
wantErr: false,
|
||||
getAuthInfoErr: user.ErrUserNotFound,
|
||||
},
|
||||
{
|
||||
name: "returns false without an error in case the auth entry is not oauth",
|
||||
user: &user.SignedInUser{UserID: 1},
|
||||
want: nil,
|
||||
wantExist: false,
|
||||
wantErr: false,
|
||||
getAuthInfoUser: login.UserAuth{AuthModule: "auth_saml"},
|
||||
},
|
||||
{
|
||||
name: "returns true when the auth entry is found",
|
||||
user: &user.SignedInUser{UserID: 1},
|
||||
want: &login.UserAuth{AuthModule: login.GenericOAuthModule},
|
||||
wantExist: true,
|
||||
wantErr: false,
|
||||
getAuthInfoUser: login.UserAuth{AuthModule: login.GenericOAuthModule},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
srv, authInfoStore, _ := setupOAuthTokenService(t)
|
||||
authInfoStore.ExpectedOAuth = &tc.getAuthInfoUser
|
||||
authInfoStore.ExpectedError = tc.getAuthInfoErr
|
||||
|
||||
entry, exists, err := srv.HasOAuthEntry(context.Background(), tc.user)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
if tc.want != nil {
|
||||
assert.True(t, reflect.DeepEqual(tc.want, entry))
|
||||
}
|
||||
assert.Equal(t, tc.wantExist, exists)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupOAuthTokenService(t *testing.T) (*Service, *FakeAuthInfoStore, *socialtest.MockSocialConnector) {
|
||||
t.Helper()
|
||||
|
||||
socialConnector := &socialtest.MockSocialConnector{}
|
||||
socialService := &socialtest.FakeSocialService{
|
||||
ExpectedConnector: socialConnector,
|
||||
ExpectedAuthInfoProvider: &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
},
|
||||
}
|
||||
|
||||
authInfoStore := &FakeAuthInfoStore{ExpectedOAuth: &login.UserAuth{}}
|
||||
authInfoService := authinfoimpl.ProvideService(authInfoStore, remotecache.NewFakeCacheStorage(), secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()))
|
||||
|
||||
store := db.InitTestDB(t)
|
||||
|
||||
return &Service{
|
||||
Cfg: setting.NewCfg(),
|
||||
SocialService: socialService,
|
||||
AuthInfoService: authInfoService,
|
||||
serverLock: serverlock.ProvideService(store, tracing.InitializeTracerForTest()),
|
||||
tokenRefreshDuration: newTokenRefreshDurationMetric(prometheus.NewRegistry()),
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
}, authInfoStore, socialConnector
|
||||
}
|
||||
|
||||
type FakeAuthInfoStore struct {
|
||||
login.Store
|
||||
ExpectedError error
|
||||
@@ -379,10 +278,12 @@ func TestService_TryTokenRefresh(t *testing.T) {
|
||||
prometheus.NewRegistry(),
|
||||
env.serverLock,
|
||||
tracing.InitializeTracerForTest(),
|
||||
nil,
|
||||
featuremgmt.WithFeatures(),
|
||||
)
|
||||
|
||||
// token refresh
|
||||
actualToken, err := env.service.TryTokenRefresh(context.Background(), tt.identity)
|
||||
actualToken, err := env.service.TryTokenRefresh(context.Background(), tt.identity, nil)
|
||||
|
||||
if tt.expectedErr != nil {
|
||||
assert.ErrorIs(t, err, tt.expectedErr)
|
||||
@@ -407,6 +308,275 @@ func TestService_TryTokenRefresh(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_TryTokenRefresh_WithExternalSessions(t *testing.T) {
|
||||
unexpiredToken := &oauth2.Token{
|
||||
AccessToken: "testaccess",
|
||||
RefreshToken: "testrefresh",
|
||||
Expiry: time.Now().Add(time.Hour),
|
||||
TokenType: "Bearer",
|
||||
}
|
||||
unexpiredTokenWithIDToken := unexpiredToken.WithExtra(map[string]interface{}{
|
||||
"id_token": UNEXPIRED_ID_TOKEN,
|
||||
})
|
||||
|
||||
expiredToken := &oauth2.Token{
|
||||
AccessToken: "testaccess",
|
||||
RefreshToken: "testrefresh",
|
||||
Expiry: time.Now().Add(-time.Hour),
|
||||
TokenType: "Bearer",
|
||||
}
|
||||
|
||||
userIdentity := &authn.Identity{
|
||||
AuthenticatedBy: login.GenericOAuthModule,
|
||||
ID: "1234",
|
||||
Type: claims.TypeUser,
|
||||
}
|
||||
|
||||
type environment struct {
|
||||
sessionService *authtest.MockUserAuthTokenService
|
||||
serverLock *serverlock.ServerLockService
|
||||
socialConnector *socialtest.MockSocialConnector
|
||||
socialService *socialtest.FakeSocialService
|
||||
|
||||
service *Service
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
desc string
|
||||
identity identity.Requester
|
||||
setup func(env *environment)
|
||||
expectedToken *oauth2.Token
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
desc: "should skip sync when identity is nil",
|
||||
},
|
||||
{
|
||||
desc: "should skip sync when identity is not a user",
|
||||
identity: &authn.Identity{ID: "1", Type: claims.TypeServiceAccount},
|
||||
},
|
||||
{
|
||||
desc: "should skip token refresh and return nil if namespace and id cannot be converted to user ID",
|
||||
identity: &authn.Identity{ID: "invalid", Type: claims.TypeUser},
|
||||
},
|
||||
{
|
||||
desc: "should skip token refresh if there's an unexpected error while looking up the user oauth entry, additionally, no error should be returned",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(nil, assert.AnError).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
// Kinda impossible to happen, can only happen after the feature is enabled and logged in users don't have their external sessions set
|
||||
{
|
||||
desc: "should skip token refresh if the user doesn't have an external session",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(nil, auth.ErrExternalSessionNotFound).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should skip token refresh when no oauth provider was found",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.socialService.ExpectedAuthInfoProvider = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should skip token refresh when oauth provider token handling is disabled (UseRefreshToken is false)",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should skip token refresh when the token is still valid and no id token is present",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(&auth.ExternalSession{
|
||||
ID: 1,
|
||||
UserID: 1,
|
||||
AccessToken: unexpiredTokenWithIDToken.AccessToken,
|
||||
RefreshToken: unexpiredTokenWithIDToken.RefreshToken,
|
||||
ExpiresAt: unexpiredTokenWithIDToken.Expiry,
|
||||
}, nil).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
},
|
||||
expectedToken: unexpiredToken,
|
||||
},
|
||||
{
|
||||
desc: "should not do token refresh if access token or id token have not expired yet",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(&auth.ExternalSession{
|
||||
ID: 1,
|
||||
UserID: 1,
|
||||
AccessToken: unexpiredTokenWithIDToken.AccessToken,
|
||||
RefreshToken: unexpiredTokenWithIDToken.RefreshToken,
|
||||
ExpiresAt: unexpiredTokenWithIDToken.Expiry,
|
||||
IDToken: UNEXPIRED_ID_TOKEN,
|
||||
}, nil).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
},
|
||||
expectedToken: unexpiredTokenWithIDToken,
|
||||
},
|
||||
{
|
||||
desc: "should skip token refresh when there is no refresh token",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(&auth.ExternalSession{
|
||||
ID: 1,
|
||||
UserID: 1,
|
||||
AccessToken: unexpiredTokenWithIDToken.AccessToken,
|
||||
RefreshToken: "",
|
||||
ExpiresAt: unexpiredTokenWithIDToken.Expiry,
|
||||
}, nil).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
},
|
||||
expectedToken: &oauth2.Token{
|
||||
AccessToken: unexpiredTokenWithIDToken.AccessToken,
|
||||
RefreshToken: "",
|
||||
Expiry: unexpiredTokenWithIDToken.Expiry,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should refresh token when the access token is expired",
|
||||
identity: &authn.Identity{
|
||||
AuthenticatedBy: login.GenericOAuthModule,
|
||||
ID: "1",
|
||||
Type: claims.TypeUser,
|
||||
},
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(&auth.ExternalSession{
|
||||
ID: 1,
|
||||
UserID: 1,
|
||||
AccessToken: expiredToken.AccessToken,
|
||||
IDToken: UNEXPIRED_ID_TOKEN,
|
||||
RefreshToken: expiredToken.RefreshToken,
|
||||
ExpiresAt: expiredToken.Expiry,
|
||||
}, nil).Once()
|
||||
|
||||
env.sessionService.On("UpdateExternalSession", mock.Anything, int64(1), mock.MatchedBy(verifyUpdateExternalSessionCommand(unexpiredTokenWithIDToken))).Return(nil).Once()
|
||||
|
||||
env.socialConnector.On("TokenSource", mock.Anything, mock.Anything).Return(oauth2.StaticTokenSource(unexpiredTokenWithIDToken)).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
},
|
||||
expectedToken: unexpiredTokenWithIDToken,
|
||||
},
|
||||
{
|
||||
desc: "should refresh token when the id token is expired",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(&auth.ExternalSession{
|
||||
ID: 1,
|
||||
UserID: 1,
|
||||
AccessToken: unexpiredTokenWithIDToken.AccessToken,
|
||||
RefreshToken: unexpiredTokenWithIDToken.RefreshToken,
|
||||
ExpiresAt: unexpiredTokenWithIDToken.Expiry,
|
||||
IDToken: EXPIRED_ID_TOKEN,
|
||||
}, nil).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
|
||||
env.sessionService.On("UpdateExternalSession", mock.Anything, int64(1), mock.MatchedBy(verifyUpdateExternalSessionCommand(unexpiredTokenWithIDToken))).Return(nil).Once()
|
||||
|
||||
env.socialConnector.On("TokenSource", mock.Anything, mock.Anything).Return(oauth2.StaticTokenSource(unexpiredTokenWithIDToken)).Once()
|
||||
},
|
||||
expectedToken: unexpiredTokenWithIDToken,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
socialConnector := socialtest.NewMockSocialConnector(t)
|
||||
|
||||
store := db.InitTestDB(t)
|
||||
|
||||
env := environment{
|
||||
sessionService: authtest.NewMockUserAuthTokenService(t),
|
||||
serverLock: serverlock.ProvideService(store, tracing.InitializeTracerForTest()),
|
||||
socialConnector: socialConnector,
|
||||
socialService: &socialtest.FakeSocialService{
|
||||
ExpectedConnector: socialConnector,
|
||||
},
|
||||
}
|
||||
|
||||
if tt.setup != nil {
|
||||
tt.setup(&env)
|
||||
}
|
||||
|
||||
env.service = ProvideService(
|
||||
env.socialService,
|
||||
nil,
|
||||
setting.NewCfg(),
|
||||
prometheus.NewRegistry(),
|
||||
env.serverLock,
|
||||
tracing.InitializeTracerForTest(),
|
||||
env.sessionService,
|
||||
featuremgmt.WithFeatures(featuremgmt.FlagImprovedExternalSessionHandling),
|
||||
)
|
||||
|
||||
// token refresh
|
||||
actualToken, err := env.service.TryTokenRefresh(context.Background(), tt.identity, &usertoken.UserToken{ExternalSessionId: 1})
|
||||
|
||||
if tt.expectedErr != nil {
|
||||
assert.ErrorIs(t, err, tt.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
if tt.expectedToken == nil {
|
||||
assert.Nil(t, actualToken)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedToken.AccessToken, actualToken.AccessToken)
|
||||
assert.Equal(t, tt.expectedToken.RefreshToken, actualToken.RefreshToken)
|
||||
assert.Equal(t, tt.expectedToken.Expiry, actualToken.Expiry)
|
||||
if tt.expectedToken.Extra("id_token") != nil {
|
||||
assert.Equal(t, tt.expectedToken.Extra("id_token").(string), actualToken.Extra("id_token").(string))
|
||||
} else {
|
||||
assert.Nil(t, actualToken.Extra("id_token"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func verifyUpdateExternalSessionCommand(token *oauth2.Token) func(*auth.UpdateExternalSessionCommand) bool {
|
||||
return func(cmd *auth.UpdateExternalSessionCommand) bool {
|
||||
idToken := cmd.Token.Extra("id_token")
|
||||
return cmd.Token.AccessToken == token.AccessToken &&
|
||||
cmd.Token.RefreshToken == token.RefreshToken &&
|
||||
cmd.Token.Expiry == token.Expiry &&
|
||||
idToken == token.Extra("id_token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOAuthTokenSync_needTokenRefresh(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -6,21 +6,20 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
)
|
||||
|
||||
type MockOauthTokenService struct {
|
||||
GetCurrentOauthTokenFunc func(ctx context.Context, usr identity.Requester) *oauth2.Token
|
||||
GetCurrentOauthTokenFunc func(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) *oauth2.Token
|
||||
IsOAuthPassThruEnabledFunc func(ds *datasources.DataSource) bool
|
||||
HasOAuthEntryFunc func(ctx context.Context, usr identity.Requester) (*login.UserAuth, bool, error)
|
||||
InvalidateOAuthTokensFunc func(ctx context.Context, usr identity.Requester) error
|
||||
TryTokenRefreshFunc func(ctx context.Context, usr identity.Requester) (*oauth2.Token, error)
|
||||
InvalidateOAuthTokensFunc func(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) error
|
||||
TryTokenRefreshFunc func(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) (*oauth2.Token, error)
|
||||
}
|
||||
|
||||
func (m *MockOauthTokenService) GetCurrentOAuthToken(ctx context.Context, usr identity.Requester) *oauth2.Token {
|
||||
func (m *MockOauthTokenService) GetCurrentOAuthToken(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) *oauth2.Token {
|
||||
if m.GetCurrentOauthTokenFunc != nil {
|
||||
return m.GetCurrentOauthTokenFunc(ctx, usr)
|
||||
return m.GetCurrentOauthTokenFunc(ctx, usr, sessionToken)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -32,23 +31,16 @@ func (m *MockOauthTokenService) IsOAuthPassThruEnabled(ds *datasources.DataSourc
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *MockOauthTokenService) HasOAuthEntry(ctx context.Context, usr identity.Requester) (*login.UserAuth, bool, error) {
|
||||
if m.HasOAuthEntryFunc != nil {
|
||||
return m.HasOAuthEntryFunc(ctx, usr)
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (m *MockOauthTokenService) InvalidateOAuthTokens(ctx context.Context, usr identity.Requester) error {
|
||||
func (m *MockOauthTokenService) InvalidateOAuthTokens(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) error {
|
||||
if m.InvalidateOAuthTokensFunc != nil {
|
||||
return m.InvalidateOAuthTokensFunc(ctx, usr)
|
||||
return m.InvalidateOAuthTokensFunc(ctx, usr, sessionToken)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockOauthTokenService) TryTokenRefresh(ctx context.Context, usr identity.Requester) (*oauth2.Token, error) {
|
||||
func (m *MockOauthTokenService) TryTokenRefresh(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) (*oauth2.Token, error) {
|
||||
if m.TryTokenRefreshFunc != nil {
|
||||
return m.TryTokenRefreshFunc(ctx, usr)
|
||||
return m.TryTokenRefreshFunc(ctx, usr, sessionToken)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken"
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ func ProvideService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) GetCurrentOAuthToken(context.Context, identity.Requester) *oauth2.Token {
|
||||
func (s *Service) GetCurrentOAuthToken(context.Context, identity.Requester, *auth.UserToken) *oauth2.Token {
|
||||
return s.Token
|
||||
}
|
||||
|
||||
@@ -29,14 +29,10 @@ func (s *Service) IsOAuthPassThruEnabled(ds *datasources.DataSource) bool {
|
||||
return oauthtoken.IsOAuthPassThruEnabled(ds)
|
||||
}
|
||||
|
||||
func (s *Service) HasOAuthEntry(context.Context, identity.Requester) (*login.UserAuth, bool, error) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (s *Service) TryTokenRefresh(context.Context, identity.Requester) (*oauth2.Token, error) {
|
||||
func (s *Service) TryTokenRefresh(context.Context, identity.Requester, *auth.UserToken) (*oauth2.Token, error) {
|
||||
return s.Token, nil
|
||||
}
|
||||
|
||||
func (s *Service) InvalidateOAuthTokens(context.Context, identity.Requester) error {
|
||||
func (s *Service) InvalidateOAuthTokens(context.Context, identity.Requester, *auth.UserToken) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.40.1. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.1. DO NOT EDIT.
|
||||
|
||||
package oauthtokentest
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
identity "github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
datasources "github.com/grafana/grafana/pkg/services/datasources"
|
||||
|
||||
login "github.com/grafana/grafana/pkg/services/login"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
oauth2 "golang.org/x/oauth2"
|
||||
|
||||
usertoken "github.com/grafana/grafana/pkg/models/usertoken"
|
||||
)
|
||||
|
||||
// MockService is an autogenerated mock type for the OAuthTokenService type
|
||||
@@ -20,17 +20,17 @@ type MockService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GetCurrentOAuthToken provides a mock function with given fields: _a0, _a1
|
||||
func (_m *MockService) GetCurrentOAuthToken(_a0 context.Context, _a1 identity.Requester) *oauth2.Token {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
// GetCurrentOAuthToken provides a mock function with given fields: _a0, _a1, _a2
|
||||
func (_m *MockService) GetCurrentOAuthToken(_a0 context.Context, _a1 identity.Requester, _a2 *usertoken.UserToken) *oauth2.Token {
|
||||
ret := _m.Called(_a0, _a1, _a2)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetCurrentOAuthToken")
|
||||
}
|
||||
|
||||
var r0 *oauth2.Token
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) *oauth2.Token); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester, *usertoken.UserToken) *oauth2.Token); ok {
|
||||
r0 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*oauth2.Token)
|
||||
@@ -40,54 +40,17 @@ func (_m *MockService) GetCurrentOAuthToken(_a0 context.Context, _a1 identity.Re
|
||||
return r0
|
||||
}
|
||||
|
||||
// HasOAuthEntry provides a mock function with given fields: _a0, _a1
|
||||
func (_m *MockService) HasOAuthEntry(_a0 context.Context, _a1 identity.Requester) (*login.UserAuth, bool, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for HasOAuthEntry")
|
||||
}
|
||||
|
||||
var r0 *login.UserAuth
|
||||
var r1 bool
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) (*login.UserAuth, bool, error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) *login.UserAuth); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*login.UserAuth)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, identity.Requester) bool); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Get(1).(bool)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(2).(func(context.Context, identity.Requester) error); ok {
|
||||
r2 = rf(_a0, _a1)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// InvalidateOAuthTokens provides a mock function with given fields: _a0, _a1
|
||||
func (_m *MockService) InvalidateOAuthTokens(_a0 context.Context, _a1 *login.UserAuth) error {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
// InvalidateOAuthTokens provides a mock function with given fields: _a0, _a1, _a2
|
||||
func (_m *MockService) InvalidateOAuthTokens(_a0 context.Context, _a1 identity.Requester, _a2 *usertoken.UserToken) error {
|
||||
ret := _m.Called(_a0, _a1, _a2)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for InvalidateOAuthTokens")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *login.UserAuth) error); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester, *usertoken.UserToken) error); ok {
|
||||
r0 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
@@ -113,22 +76,34 @@ func (_m *MockService) IsOAuthPassThruEnabled(_a0 *datasources.DataSource) bool
|
||||
return r0
|
||||
}
|
||||
|
||||
// TryTokenRefresh provides a mock function with given fields: _a0, _a1
|
||||
func (_m *MockService) TryTokenRefresh(_a0 context.Context, _a1 identity.Requester) error {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
// TryTokenRefresh provides a mock function with given fields: _a0, _a1, _a2
|
||||
func (_m *MockService) TryTokenRefresh(_a0 context.Context, _a1 identity.Requester, _a2 *usertoken.UserToken) (*oauth2.Token, error) {
|
||||
ret := _m.Called(_a0, _a1, _a2)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for TryTokenRefresh")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) error); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
var r0 *oauth2.Token
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester, *usertoken.UserToken) (*oauth2.Token, error)); ok {
|
||||
return rf(_a0, _a1, _a2)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester, *usertoken.UserToken) *oauth2.Token); ok {
|
||||
r0 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*oauth2.Token)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
if rf, ok := ret.Get(1).(func(context.Context, identity.Requester, *usertoken.UserToken) error); ok {
|
||||
r1 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
|
||||
@@ -50,7 +50,7 @@ func (m *OAuthTokenMiddleware) applyToken(ctx context.Context, pCtx backend.Plug
|
||||
}
|
||||
|
||||
if m.oAuthTokenService.IsOAuthPassThruEnabled(ds) {
|
||||
if token := m.oAuthTokenService.GetCurrentOAuthToken(ctx, reqCtx.SignedInUser); token != nil {
|
||||
if token := m.oAuthTokenService.GetCurrentOAuthToken(ctx, reqCtx.SignedInUser, reqCtx.UserToken); token != nil {
|
||||
authorizationHeader := fmt.Sprintf("%s %s", token.Type(), token.AccessToken)
|
||||
idTokenHeader := ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user