2022-10-19 00:17:28 +08:00
package oauthtoken
import (
"context"
"errors"
"reflect"
"testing"
"time"
2024-08-13 16:18:28 +08:00
"github.com/grafana/authlib/claims"
2023-10-05 17:19:43 +08:00
"github.com/prometheus/client_golang/prometheus"
2023-01-28 02:36:54 +08:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"golang.org/x/oauth2"
2024-06-13 12:11:35 +08:00
"github.com/grafana/grafana/pkg/apimachinery/identity"
2024-08-20 00:57:37 +08:00
"github.com/grafana/grafana/pkg/infra/db"
2024-01-23 22:26:38 +08:00
"github.com/grafana/grafana/pkg/infra/remotecache"
2024-08-20 00:57:37 +08:00
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/tracing"
2024-02-05 23:44:25 +08:00
"github.com/grafana/grafana/pkg/login/social"
2023-12-08 18:20:42 +08:00
"github.com/grafana/grafana/pkg/login/social/socialtest"
2024-02-05 23:44:25 +08:00
"github.com/grafana/grafana/pkg/services/authn"
2022-10-19 00:17:28 +08:00
"github.com/grafana/grafana/pkg/services/login"
2023-11-21 21:47:23 +08:00
"github.com/grafana/grafana/pkg/services/login/authinfoimpl"
2024-02-05 23:44:25 +08:00
"github.com/grafana/grafana/pkg/services/login/authinfotest"
2024-01-23 22:26:38 +08:00
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
2022-10-19 00:17:28 +08:00
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
2024-08-20 00:57:37 +08:00
"github.com/grafana/grafana/pkg/tests/testsuite"
2022-10-19 00:17:28 +08:00
)
2024-11-21 21:36:28 +08:00
const EXPIRED_ID_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6InlvdXItY2xpZW50LWlkIiwiZXhwIjoxNjAwMDAwMDAwLCJpYXQiOjE2MDAwMDAwMDAsIm5hbWUiOiJKb2huIERvZSIsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSJ9.c2lnbmF0dXJl" // #nosec G101 not a hardcoded credential
const UNEXPIRED_ID_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6InlvdXItY2xpZW50LWlkIiwiZXhwIjo0ODg1NjA4MDAwLCJpYXQiOjE2ODU2MDgwMDAsIm5hbWUiOiJKb2huIERvZSIsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSJ9.c2lnbmF0dXJl" // #nosec G101 not a hardcoded credential
2024-02-05 23:44:25 +08:00
2024-08-20 00:57:37 +08:00
func TestMain ( m * testing . M ) {
testsuite . Run ( m )
}
2022-10-19 00:17:28 +08:00
func TestService_HasOAuthEntry ( t * testing . T ) {
testCases := [ ] struct {
name string
user * user . SignedInUser
2023-01-28 02:36:54 +08:00
want * login . UserAuth
2022-10-19 00:17:28 +08:00
wantExist bool
wantErr bool
err error
getAuthInfoErr error
2023-01-28 02:36:54 +08:00
getAuthInfoUser login . UserAuth
2022-10-19 00:17:28 +08:00
} {
{
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" ,
2023-08-29 17:55:58 +08:00
user : & user . SignedInUser { UserID : 1 } ,
2022-10-19 00:17:28 +08:00
want : nil ,
wantExist : false ,
wantErr : true ,
getAuthInfoErr : errors . New ( "error" ) ,
} ,
{
name : "returns false without an error in case auth entry is not found" ,
2023-08-29 17:55:58 +08:00
user : & user . SignedInUser { UserID : 1 } ,
2022-10-19 00:17:28 +08:00
want : nil ,
wantExist : false ,
wantErr : false ,
getAuthInfoErr : user . ErrUserNotFound ,
} ,
{
name : "returns false without an error in case the auth entry is not oauth" ,
2023-08-29 17:55:58 +08:00
user : & user . SignedInUser { UserID : 1 } ,
2022-10-19 00:17:28 +08:00
want : nil ,
wantExist : false ,
wantErr : false ,
2023-01-28 02:36:54 +08:00
getAuthInfoUser : login . UserAuth { AuthModule : "auth_saml" } ,
2022-10-19 00:17:28 +08:00
} ,
{
name : "returns true when the auth entry is found" ,
2023-08-29 17:55:58 +08:00
user : & user . SignedInUser { UserID : 1 } ,
2024-02-05 23:44:25 +08:00
want : & login . UserAuth { AuthModule : login . GenericOAuthModule } ,
2022-10-19 00:17:28 +08:00
wantExist : true ,
wantErr : false ,
2024-02-05 23:44:25 +08:00
getAuthInfoUser : login . UserAuth { AuthModule : login . GenericOAuthModule } ,
2022-10-19 00:17:28 +08:00
} ,
}
for _ , tc := range testCases {
2023-08-29 17:55:58 +08:00
tc := tc
2022-10-19 00:17:28 +08:00
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 )
} )
}
}
2023-07-14 20:03:01 +08:00
func setupOAuthTokenService ( t * testing . T ) ( * Service , * FakeAuthInfoStore , * socialtest . MockSocialConnector ) {
2022-10-19 00:17:28 +08:00
t . Helper ( )
2023-07-14 20:03:01 +08:00
socialConnector := & socialtest . MockSocialConnector { }
socialService := & socialtest . FakeSocialService {
ExpectedConnector : socialConnector ,
2024-02-05 23:44:25 +08:00
ExpectedAuthInfoProvider : & social . OAuthInfo {
UseRefreshToken : true ,
} ,
2022-10-19 00:17:28 +08:00
}
2024-02-05 23:44:25 +08:00
authInfoStore := & FakeAuthInfoStore { ExpectedOAuth : & login . UserAuth { } }
authInfoService := authinfoimpl . ProvideService ( authInfoStore , remotecache . NewFakeCacheStorage ( ) , secretsManager . SetupTestService ( t , fakes . NewFakeSecretsStore ( ) ) )
2024-08-20 00:57:37 +08:00
store := db . InitTestDB ( t )
2022-10-19 00:17:28 +08:00
return & Service {
2023-10-05 17:19:43 +08:00
Cfg : setting . NewCfg ( ) ,
SocialService : socialService ,
AuthInfoService : authInfoService ,
2024-08-20 00:57:37 +08:00
serverLock : serverlock . ProvideService ( store , tracing . InitializeTracerForTest ( ) ) ,
2023-10-05 17:19:43 +08:00
tokenRefreshDuration : newTokenRefreshDurationMetric ( prometheus . NewRegistry ( ) ) ,
2024-08-20 00:57:37 +08:00
tracer : tracing . InitializeTracerForTest ( ) ,
2022-10-19 00:17:28 +08:00
} , authInfoStore , socialConnector
}
type FakeAuthInfoStore struct {
2022-11-29 22:20:28 +08:00
login . Store
2023-11-21 21:47:23 +08:00
ExpectedError error
ExpectedOAuth * login . UserAuth
2022-10-19 00:17:28 +08:00
}
2023-03-29 02:32:21 +08:00
func ( f * FakeAuthInfoStore ) GetAuthInfo ( ctx context . Context , query * login . GetAuthInfoQuery ) ( * login . UserAuth , error ) {
return f . ExpectedOAuth , f . ExpectedError
2022-10-19 00:17:28 +08:00
}
2023-01-28 02:36:54 +08:00
func ( f * FakeAuthInfoStore ) SetAuthInfo ( ctx context . Context , cmd * login . SetAuthInfoCommand ) error {
2022-10-19 00:17:28 +08:00
return f . ExpectedError
}
2023-01-28 02:36:54 +08:00
func ( f * FakeAuthInfoStore ) UpdateAuthInfo ( ctx context . Context , cmd * login . UpdateAuthInfoCommand ) error {
2022-10-19 00:17:28 +08:00
f . ExpectedOAuth . OAuthAccessToken = cmd . OAuthToken . AccessToken
f . ExpectedOAuth . OAuthExpiry = cmd . OAuthToken . Expiry
f . ExpectedOAuth . OAuthTokenType = cmd . OAuthToken . TokenType
f . ExpectedOAuth . OAuthRefreshToken = cmd . OAuthToken . RefreshToken
return f . ExpectedError
}
2023-01-28 02:36:54 +08:00
func ( f * FakeAuthInfoStore ) DeleteAuthInfo ( ctx context . Context , cmd * login . DeleteAuthInfoCommand ) error {
2022-10-19 00:17:28 +08:00
return f . ExpectedError
}
2024-02-05 23:44:25 +08:00
func TestService_TryTokenRefresh ( t * testing . T ) {
2024-11-21 21:36:28 +08:00
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" ,
}
2024-02-05 23:44:25 +08:00
type environment struct {
authInfoService * authinfotest . FakeService
2024-08-20 00:57:37 +08:00
serverLock * serverlock . ServerLockService
2024-02-05 23:44:25 +08:00
socialConnector * socialtest . MockSocialConnector
socialService * socialtest . FakeSocialService
service * Service
}
2024-11-21 21:36:28 +08:00
2024-02-05 23:44:25 +08:00
type testCase struct {
2024-11-21 21:36:28 +08:00
desc string
identity identity . Requester
setup func ( env * environment )
expectedToken * oauth2 . Token
expectedErr error
}
userIdentity := & authn . Identity {
AuthenticatedBy : login . GenericOAuthModule ,
ID : "1234" ,
Type : claims . TypeUser ,
2024-02-05 23:44:25 +08:00
}
tests := [ ] testCase {
{
desc : "should skip sync when identity is nil" ,
} ,
{
2024-11-21 21:36:28 +08:00
desc : "should skip sync when identity is not a user" ,
identity : & authn . Identity { ID : "1" , Type : claims . TypeServiceAccount } ,
2024-02-05 23:44:25 +08:00
} ,
{
2024-11-21 21:36:28 +08:00
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 } ,
2024-02-05 23:44:25 +08:00
} ,
{
2024-11-21 21:36:28 +08:00
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 ,
2024-02-05 23:44:25 +08:00
setup : func ( env * environment ) {
2024-11-21 21:36:28 +08:00
env . authInfoService . ExpectedError = errors . New ( "some error" )
2024-02-05 23:44:25 +08:00
} ,
} ,
{
2024-11-21 21:36:28 +08:00
desc : "should skip token refresh if the user doesn't have an oauth entry" ,
identity : userIdentity ,
2024-02-05 23:44:25 +08:00
setup : func ( env * environment ) {
2024-11-21 21:36:28 +08:00
env . authInfoService . ExpectedUserAuth = & login . UserAuth {
AuthModule : login . SAMLAuthModule ,
}
2024-02-05 23:44:25 +08:00
} ,
} ,
{
2024-11-21 21:36:28 +08:00
desc : "should skip token refresh when no oauth provider was found" ,
identity : userIdentity ,
2024-02-05 23:44:25 +08:00
setup : func ( env * environment ) {
env . authInfoService . ExpectedUserAuth = & login . UserAuth {
2024-11-21 21:36:28 +08:00
AuthModule : login . GenericOAuthModule ,
2024-02-05 23:44:25 +08:00
}
} ,
} ,
{
2024-11-21 21:36:28 +08:00
desc : "should skip token refresh when oauth provider token handling is disabled (UseRefreshToken is false)" ,
identity : userIdentity ,
2024-02-05 23:44:25 +08:00
setup : func ( env * environment ) {
env . authInfoService . ExpectedUserAuth = & login . UserAuth {
AuthModule : login . GenericOAuthModule ,
}
2024-11-21 21:36:28 +08:00
env . socialService . ExpectedAuthInfoProvider = & social . OAuthInfo {
UseRefreshToken : false ,
}
2024-02-05 23:44:25 +08:00
} ,
} ,
{
2024-11-21 21:36:28 +08:00
desc : "should skip token refresh when the token is still valid and no id token is present" ,
identity : userIdentity ,
2024-02-05 23:44:25 +08:00
setup : func ( env * environment ) {
env . authInfoService . ExpectedUserAuth = & login . UserAuth {
2024-11-21 21:36:28 +08:00
AuthModule : login . GenericOAuthModule ,
OAuthAccessToken : unexpiredTokenWithIDToken . AccessToken ,
OAuthRefreshToken : unexpiredTokenWithIDToken . RefreshToken ,
OAuthExpiry : unexpiredTokenWithIDToken . Expiry ,
OAuthTokenType : unexpiredTokenWithIDToken . TokenType ,
}
env . socialService . ExpectedAuthInfoProvider = & social . OAuthInfo {
UseRefreshToken : true ,
2024-02-05 23:44:25 +08:00
}
} ,
2024-11-21 21:36:28 +08:00
expectedToken : unexpiredToken ,
2024-02-05 23:44:25 +08:00
} ,
{
2024-11-21 21:36:28 +08:00
desc : "should not refresh the tokens if access token or id token have not expired yet" ,
identity : userIdentity ,
2024-02-05 23:44:25 +08:00
setup : func ( env * environment ) {
env . authInfoService . ExpectedUserAuth = & login . UserAuth {
2024-11-21 21:36:28 +08:00
AuthModule : login . GenericOAuthModule ,
OAuthIdToken : UNEXPIRED_ID_TOKEN ,
OAuthAccessToken : unexpiredTokenWithIDToken . AccessToken ,
OAuthRefreshToken : unexpiredTokenWithIDToken . RefreshToken ,
OAuthExpiry : unexpiredTokenWithIDToken . Expiry ,
OAuthTokenType : unexpiredTokenWithIDToken . TokenType ,
2024-02-05 23:44:25 +08:00
}
2024-11-21 21:36:28 +08:00
2024-02-05 23:44:25 +08:00
env . socialService . ExpectedAuthInfoProvider = & social . OAuthInfo {
2024-11-21 21:36:28 +08:00
UseRefreshToken : true ,
2024-02-05 23:44:25 +08:00
}
} ,
2024-11-21 21:36:28 +08:00
expectedToken : unexpiredTokenWithIDToken ,
2024-02-05 23:44:25 +08:00
} ,
{
2024-11-21 21:36:28 +08:00
desc : "should skip token refresh when there is no refresh token" ,
identity : userIdentity ,
2024-02-05 23:44:25 +08:00
setup : func ( env * environment ) {
env . authInfoService . ExpectedUserAuth = & login . UserAuth {
AuthModule : login . GenericOAuthModule ,
2024-11-21 21:36:28 +08:00
OAuthAccessToken : unexpiredTokenWithIDToken . AccessToken ,
2024-02-05 23:44:25 +08:00
OAuthRefreshToken : "" ,
2024-11-21 21:36:28 +08:00
OAuthExpiry : unexpiredTokenWithIDToken . Expiry ,
2024-02-05 23:44:25 +08:00
}
env . socialService . ExpectedAuthInfoProvider = & social . OAuthInfo {
UseRefreshToken : true ,
}
} ,
2024-11-21 21:36:28 +08:00
expectedToken : & oauth2 . Token {
AccessToken : unexpiredTokenWithIDToken . AccessToken ,
RefreshToken : "" ,
Expiry : unexpiredTokenWithIDToken . Expiry ,
} ,
2024-02-05 23:44:25 +08:00
} ,
{
2024-11-21 21:36:28 +08:00
desc : "should do token refresh when the token is expired" ,
identity : userIdentity ,
2024-02-05 23:44:25 +08:00
setup : func ( env * environment ) {
env . socialService . ExpectedAuthInfoProvider = & social . OAuthInfo {
UseRefreshToken : true ,
}
env . authInfoService . ExpectedUserAuth = & login . UserAuth {
AuthModule : login . GenericOAuthModule ,
AuthId : "subject" ,
UserId : 1 ,
2024-11-21 21:36:28 +08:00
OAuthAccessToken : expiredToken . AccessToken ,
OAuthRefreshToken : expiredToken . RefreshToken ,
OAuthExpiry : expiredToken . Expiry ,
OAuthTokenType : expiredToken . TokenType ,
OAuthIdToken : EXPIRED_ID_TOKEN ,
2024-02-05 23:44:25 +08:00
}
2024-11-21 21:36:28 +08:00
env . socialConnector . On ( "TokenSource" , mock . Anything , mock . Anything ) . Return ( oauth2 . StaticTokenSource ( unexpiredTokenWithIDToken ) ) . Once ( )
2024-02-05 23:44:25 +08:00
} ,
2024-11-21 21:36:28 +08:00
expectedToken : unexpiredTokenWithIDToken ,
2024-02-05 23:44:25 +08:00
} ,
{
2024-11-21 21:36:28 +08:00
desc : "should refresh token when the id token is expired" ,
identity : & authn . Identity { ID : "1234" , Type : claims . TypeUser , AuthenticatedBy : login . GenericOAuthModule } ,
2024-02-05 23:44:25 +08:00
setup : func ( env * environment ) {
env . socialService . ExpectedAuthInfoProvider = & social . OAuthInfo {
UseRefreshToken : true ,
}
env . authInfoService . ExpectedUserAuth = & login . UserAuth {
AuthModule : login . GenericOAuthModule ,
AuthId : "subject" ,
UserId : 1 ,
2024-11-21 21:36:28 +08:00
OAuthAccessToken : unexpiredTokenWithIDToken . AccessToken ,
OAuthRefreshToken : unexpiredTokenWithIDToken . RefreshToken ,
OAuthExpiry : unexpiredTokenWithIDToken . Expiry ,
OAuthTokenType : unexpiredTokenWithIDToken . TokenType ,
OAuthIdToken : EXPIRED_ID_TOKEN ,
2024-02-05 23:44:25 +08:00
}
2024-11-21 21:36:28 +08:00
env . socialConnector . On ( "TokenSource" , mock . Anything , mock . Anything ) . Return ( oauth2 . StaticTokenSource ( unexpiredTokenWithIDToken ) ) . Once ( )
2024-02-05 23:44:25 +08:00
} ,
2024-11-21 21:36:28 +08:00
expectedToken : unexpiredTokenWithIDToken ,
2024-02-05 23:44:25 +08:00
} ,
}
for _ , tt := range tests {
t . Run ( tt . desc , func ( t * testing . T ) {
2024-11-21 21:36:28 +08:00
socialConnector := socialtest . NewMockSocialConnector ( t )
2024-02-05 23:44:25 +08:00
2024-08-20 00:57:37 +08:00
store := db . InitTestDB ( t )
2024-02-05 23:44:25 +08:00
env := environment {
authInfoService : & authinfotest . FakeService { } ,
2024-08-20 00:57:37 +08:00
serverLock : serverlock . ProvideService ( store , tracing . InitializeTracerForTest ( ) ) ,
2024-02-05 23:44:25 +08:00
socialConnector : socialConnector ,
socialService : & socialtest . FakeSocialService {
ExpectedConnector : socialConnector ,
} ,
}
if tt . setup != nil {
tt . setup ( & env )
}
2024-08-20 00:57:37 +08:00
env . service = ProvideService (
env . socialService ,
env . authInfoService ,
setting . NewCfg ( ) ,
prometheus . NewRegistry ( ) ,
env . serverLock ,
tracing . InitializeTracerForTest ( ) ,
)
2024-02-05 23:44:25 +08:00
// token refresh
2024-11-21 21:36:28 +08:00
actualToken , err := env . service . TryTokenRefresh ( context . Background ( ) , tt . identity )
2024-02-05 23:44:25 +08:00
2024-11-21 21:36:28 +08:00
if tt . expectedErr != nil {
assert . ErrorIs ( t , err , tt . expectedErr )
return
}
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 )
assert . Equal ( t , tt . expectedToken . TokenType , actualToken . TokenType )
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" ) )
}
2024-02-05 23:44:25 +08:00
} )
}
}
func TestOAuthTokenSync_needTokenRefresh ( t * testing . T ) {
tests := [ ] struct {
name string
usr * login . UserAuth
expectedTokenRefreshFlag bool
expectedTokenDuration time . Duration
} {
{
name : "should not need token refresh when token has no expiration date" ,
usr : & login . UserAuth { } ,
expectedTokenRefreshFlag : false ,
} ,
{
name : "should not need token refresh with an invalid jwt token that might result in an error when parsing" ,
usr : & login . UserAuth {
OAuthIdToken : "invalid_jwt_format" ,
} ,
expectedTokenRefreshFlag : false ,
} ,
{
name : "should flag token refresh with id token is expired" ,
usr : & login . UserAuth {
2024-11-21 21:36:28 +08:00
OAuthIdToken : EXPIRED_ID_TOKEN ,
2024-02-05 23:44:25 +08:00
} ,
expectedTokenRefreshFlag : true ,
expectedTokenDuration : time . Second ,
} ,
{
name : "should flag token refresh when expiry date is zero" ,
usr : & login . UserAuth {
OAuthExpiry : time . Unix ( 0 , 0 ) ,
} ,
expectedTokenRefreshFlag : true ,
expectedTokenDuration : time . Second ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
2024-11-21 21:36:28 +08:00
token := buildOAuthTokenFromAuthInfo ( tt . usr )
needsTokenRefresh := needTokenRefresh ( context . Background ( ) , token )
2024-02-05 23:44:25 +08:00
assert . Equal ( t , tt . expectedTokenRefreshFlag , needsTokenRefresh )
} )
}
}