2023-10-20 22:09:46 +08:00
|
|
|
package sync
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
2024-08-20 00:57:37 +08:00
|
|
|
"golang.org/x/oauth2"
|
2023-10-26 00:15:41 +08:00
|
|
|
"golang.org/x/sync/singleflight"
|
2023-10-20 22:09:46 +08:00
|
|
|
|
2025-01-21 17:06:55 +08:00
|
|
|
claims "github.com/grafana/authlib/types"
|
|
|
|
|
2024-07-25 17:52:14 +08:00
|
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
2024-08-20 00:57:37 +08:00
|
|
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
2023-10-20 22:09:46 +08:00
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
2024-07-03 14:08:57 +08:00
|
|
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
2023-10-20 22:09:46 +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"
|
2023-10-20 22:09:46 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/auth"
|
|
|
|
"github.com/grafana/grafana/pkg/services/auth/authtest"
|
|
|
|
"github.com/grafana/grafana/pkg/services/authn"
|
2024-11-27 18:06:39 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
|
|
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
|
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
2023-10-20 22:09:46 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/login"
|
2025-10-07 16:52:43 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/oauthtoken"
|
2023-10-20 22:09:46 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) {
|
|
|
|
type testCase struct {
|
|
|
|
desc string
|
|
|
|
identity *authn.Identity
|
|
|
|
oauthInfo *social.OAuthInfo
|
|
|
|
|
2024-08-20 00:57:37 +08:00
|
|
|
expectToken *login.UserAuth
|
2023-10-20 22:09:46 +08:00
|
|
|
|
|
|
|
expectedTryRefreshErr error
|
|
|
|
expectTryRefreshTokenCalled bool
|
|
|
|
|
2024-08-20 00:57:37 +08:00
|
|
|
expectRevokeTokenCalled bool
|
2023-10-20 22:09:46 +08:00
|
|
|
|
|
|
|
expectedErr error
|
|
|
|
}
|
|
|
|
|
|
|
|
tests := []testCase{
|
|
|
|
{
|
2024-02-05 23:44:25 +08:00
|
|
|
desc: "should skip sync when identity is not a user",
|
2024-08-13 16:18:28 +08:00
|
|
|
identity: &authn.Identity{ID: "1", Type: claims.TypeServiceAccount},
|
2024-02-05 23:44:25 +08:00
|
|
|
expectTryRefreshTokenCalled: false,
|
2023-10-20 22:09:46 +08:00
|
|
|
},
|
|
|
|
{
|
2024-02-05 23:44:25 +08:00
|
|
|
desc: "should skip sync when identity is a user but is not authenticated with session token",
|
2024-08-13 16:18:28 +08:00
|
|
|
identity: &authn.Identity{ID: "1", Type: claims.TypeUser},
|
2024-02-05 23:44:25 +08:00
|
|
|
expectTryRefreshTokenCalled: false,
|
2023-10-20 22:09:46 +08:00
|
|
|
},
|
|
|
|
{
|
2024-08-20 00:57:37 +08:00
|
|
|
desc: "should invalidate access token and session token if token refresh fails",
|
|
|
|
identity: &authn.Identity{ID: "1", Type: claims.TypeUser, SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule},
|
|
|
|
expectedTryRefreshErr: errors.New("some err"),
|
|
|
|
expectTryRefreshTokenCalled: true,
|
|
|
|
expectRevokeTokenCalled: true,
|
|
|
|
expectToken: &login.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)},
|
|
|
|
expectedErr: authn.ErrExpiredAccessToken,
|
2023-10-20 22:09:46 +08:00
|
|
|
},
|
|
|
|
{
|
2024-08-20 00:57:37 +08:00
|
|
|
desc: "should refresh the token successfully",
|
|
|
|
identity: &authn.Identity{ID: "1", Type: claims.TypeUser, SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule},
|
|
|
|
expectTryRefreshTokenCalled: true,
|
|
|
|
expectRevokeTokenCalled: false,
|
2024-02-05 23:44:25 +08:00
|
|
|
},
|
|
|
|
{
|
2024-08-20 00:57:37 +08:00
|
|
|
desc: "should not invalidate the token if the token has already been refreshed by another request (singleflight)",
|
|
|
|
identity: &authn.Identity{ID: "1", Type: claims.TypeUser, SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule},
|
|
|
|
expectTryRefreshTokenCalled: true,
|
|
|
|
expectRevokeTokenCalled: false,
|
|
|
|
expectToken: &login.UserAuth{OAuthExpiry: time.Now().Add(10 * time.Minute)},
|
2023-10-20 22:09:46 +08:00
|
|
|
},
|
2025-10-07 16:52:43 +08:00
|
|
|
{
|
|
|
|
desc: "should not invalidate session if token refresh fails with no refresh token",
|
|
|
|
identity: &authn.Identity{ID: "1", Type: claims.TypeUser, SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule},
|
|
|
|
expectedTryRefreshErr: oauthtoken.ErrNoRefreshTokenFound,
|
|
|
|
expectTryRefreshTokenCalled: true,
|
|
|
|
expectRevokeTokenCalled: true,
|
|
|
|
expectedErr: oauthtoken.ErrNoRefreshTokenFound,
|
|
|
|
},
|
2024-02-05 23:44:25 +08:00
|
|
|
|
|
|
|
// TODO: address coverage of oauthtoken sync
|
2023-10-20 22:09:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
|
|
var (
|
2024-08-20 00:57:37 +08:00
|
|
|
tryRefreshCalled bool
|
|
|
|
revokeTokenCalled bool
|
2023-10-20 22:09:46 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
service := &oauthtokentest.MockOauthTokenService{
|
2025-10-07 16:52:43 +08:00
|
|
|
TryTokenRefreshFunc: func(ctx context.Context, usr identity.Requester, _ *oauthtoken.TokenRefreshMetadata) (*oauth2.Token, error) {
|
2023-10-20 22:09:46 +08:00
|
|
|
tryRefreshCalled = true
|
2024-08-20 00:57:37 +08:00
|
|
|
return nil, tt.expectedTryRefreshErr
|
2023-10-20 22:09:46 +08:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
sessionService := &authtest.FakeUserAuthTokenService{
|
|
|
|
RevokeTokenProvider: func(ctx context.Context, token *auth.UserToken, soft bool) error {
|
|
|
|
revokeTokenCalled = true
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if tt.oauthInfo == nil {
|
|
|
|
tt.oauthInfo = &social.OAuthInfo{
|
|
|
|
UseRefreshToken: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
socialService := &socialtest.FakeSocialService{
|
|
|
|
ExpectedAuthInfoProvider: tt.oauthInfo,
|
|
|
|
}
|
|
|
|
|
|
|
|
sync := &OAuthTokenSync{
|
2024-02-05 23:44:25 +08:00
|
|
|
log: log.NewNopLogger(),
|
|
|
|
service: service,
|
|
|
|
sessionService: sessionService,
|
|
|
|
socialService: socialService,
|
|
|
|
singleflightGroup: new(singleflight.Group),
|
2024-07-03 14:08:57 +08:00
|
|
|
tracer: tracing.InitializeTracerForTest(),
|
2024-08-20 00:57:37 +08:00
|
|
|
cache: localcache.New(maxOAuthTokenCacheTTL, 15*time.Minute),
|
2024-11-27 18:06:39 +08:00
|
|
|
features: featuremgmt.WithFeatures(),
|
2023-10-20 22:09:46 +08:00
|
|
|
}
|
|
|
|
|
2024-11-27 18:06:39 +08:00
|
|
|
ctx := context.Background()
|
|
|
|
reqCtx := context.WithValue(ctx, ctxkey.Key{}, &contextmodel.ReqContext{UserToken: nil})
|
|
|
|
|
|
|
|
err := sync.SyncOauthTokenHook(reqCtx, tt.identity, nil)
|
2023-10-20 22:09:46 +08:00
|
|
|
assert.ErrorIs(t, err, tt.expectedErr)
|
|
|
|
assert.Equal(t, tt.expectTryRefreshTokenCalled, tryRefreshCalled)
|
|
|
|
assert.Equal(t, tt.expectRevokeTokenCalled, revokeTokenCalled)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|