OAuth: Introduce user_refresh_token setting and make it default for the selected providers (#71533)

* First changes

* WIP docs

* Align current tests

* Add test for UseRefreshToken

* Update docs

* Fix

* Remove unnecessary AuthCodeURL from generic_oauth

* Change GitHub to disable use_refresh_token by default
This commit is contained in:
Misi 2023-07-14 14:03:01 +02:00 committed by GitHub
parent 1f3aa099d5
commit dcf26564db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 333 additions and 96 deletions

View File

@ -600,6 +600,8 @@ tls_skip_verify_insecure = false
tls_client_cert =
tls_client_key =
tls_client_ca =
# GitHub OAuth apps does not provide refresh tokens and the access tokens never expires.
use_refresh_token = false
#################################### GitLab Auth #########################
[auth.gitlab]
@ -625,6 +627,7 @@ tls_client_cert =
tls_client_key =
tls_client_ca =
use_pkce = true
use_refresh_token = true
#################################### Google Auth #########################
[auth.google]
@ -647,6 +650,7 @@ tls_client_cert =
tls_client_key =
tls_client_ca =
use_pkce = true
use_refresh_token = true
#################################### Grafana.com Auth ####################
# legacy key names (so they work in env variables)
@ -657,6 +661,7 @@ client_id = some_id
client_secret =
scopes = user:email
allowed_organizations =
use_refresh_token = false
[auth.grafana_com]
name = Grafana.com
@ -669,6 +674,7 @@ client_secret =
scopes = user:email
allowed_organizations =
skip_org_role_sync = false
use_refresh_token = false
#################################### Azure AD OAuth #######################
[auth.azuread]
@ -694,6 +700,7 @@ tls_client_key =
tls_client_ca =
use_pkce = true
skip_org_role_sync = false
use_refresh_token = true
#################################### Okta OAuth #######################
[auth.okta]
@ -719,6 +726,7 @@ tls_client_cert =
tls_client_key =
tls_client_ca =
use_pkce = true
use_refresh_token = false
#################################### Generic OAuth #######################
[auth.generic_oauth]
@ -756,6 +764,7 @@ use_pkce = false
auth_style =
allow_assign_grafana_admin = false
skip_org_role_sync = false
use_refresh_token = false
#################################### Basic Auth ##########################
[auth.basic]

View File

@ -21,6 +21,7 @@ The Azure AD authentication allows you to use an Azure Active Directory tenant a
- [Assign server administrator privileges](#assign-server-administrator-privileges)
- [Enable Azure AD OAuth in Grafana](#enable-azure-ad-oauth-in-grafana)
- [Configure refresh token](#configure-refresh-token)
- [Configure allowed tenants](#configure-allowed-tenants)
- [Configure allowed groups](#configure-allowed-groups)
- [Configure allowed domains](#configure-allowed-domains)
- [PKCE](#pkce)
@ -176,7 +177,9 @@ When a user logs in using an OAuth provider, Grafana verifies that the access to
Grafana uses a refresh token to obtain a new access token without requiring the user to log in again. If a refresh token doesn't exist, Grafana logs the user out of the system after the access token has expired.
To enable a refresh token for AzureAD, extend the `scopes` in `[auth.azuread]` with `offline_access`.
Refresh token fetching and access token expiration check is enabled by default for the AzureAD provider since Grafana v10.1.0 if the `accessTokenExpirationCheck` feature toggle is enabled. If you would like to disable access token expiration check then set the `use_refresh_token` configuration value to `false`.
> **Note:** The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.2.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check.
### Configure allowed tenants

View File

@ -65,7 +65,9 @@ To integrate your OAuth2 provider with Grafana using our generic OAuth2 authenti
b. Extend the `scopes` field of `[auth.generic_oauth]` section in Grafana configuration file with refresh token scope used by your OAuth2 provider.
c. Enable the refresh token on the provider if required.
c. Set `use_refresh_token` to `true` in `[auth.generic_oauth]` section in Grafana configuration file.
d. Enable the refresh token on the provider if required.
1. [Configure role mapping]({{< relref "#configure-role-mapping" >}}).
1. Optional: [Configure team synchronization]({{< relref "#configure-team-synchronization" >}}).
@ -113,6 +115,7 @@ The following table outlines the various generic OAuth2 configuration options. Y
| `tls_client_key` | No | The path to the key. | |
| `tls_client_ca` | No | The path to the trusted certificate authority list. | |
| `use_pkce` | No | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `false` |
| `use_refresh_token` | No | Set to `true` to use refresh token and check access token expiration. The `accessTokenExpirationCheck` feature toggle should also be enabled to use refresh token. | `false` |
### Configure login
@ -169,11 +172,13 @@ When a user logs in using an OAuth2 provider, Grafana verifies that the access t
Grafana uses a refresh token to obtain a new access token without requiring the user to log in again. If a refresh token doesn't exist, Grafana logs the user out of the system after the access token has expired.
To configure generic OAuth2 to use a refresh token, perform one or both of the following steps, if required:
To configure generic OAuth2 to use a refresh token, set `use_refresh_token` configuration option to `true` and perform one or both of the following steps, if required:
1. Extend the `scopes` field of `[auth.generic_oauth]` section in Grafana configuration file with additional scopes.
1. Enable the refresh token on the provider.
> **Note:** The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.2.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check.
## Configure role mapping
Unless `skip_org_role_sync` option is enabled, the user's role will be set to the role retrieved from the auth provider upon user login.
@ -336,6 +341,7 @@ To set up generic OAuth2 authentication with Auth0, follow these steps:
token_url = https://<domain>/oauth/token
api_url = https://<domain>/userinfo
use_pkce = true
use_refresh_token = true
```
### Set up OAuth2 with Bitbucket
@ -368,6 +374,7 @@ To set up generic OAuth2 authentication with Bitbucket, follow these steps:
team_ids_attribute_path = values[*].workspace.slug
team_ids =
allowed_organizations =
use_refresh_token = true
```
By default, a refresh token is included in the response for the **Authorization Code Grant**.

View File

@ -104,6 +104,10 @@ Grafana uses a refresh token to obtain a new access token without requiring the
By default, GitLab provides a refresh token.
Refresh token fetching and access token expiration check is enabled by default for the GitLab provider since Grafana v10.1.0 if the `accessTokenExpirationCheck` feature toggle is enabled. If you would like to disable access token expiration check then set the `use_refresh_token` configuration value to `false`.
> **Note:** The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.2.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check.
### allowed_groups
To limit access to authenticated users that are members of one or more [GitLab

View File

@ -82,6 +82,10 @@ Grafana uses a refresh token to obtain a new access token without requiring the
By default, Grafana includes the `access_type=offline` parameter in the authorization request to request a refresh token.
Refresh token fetching and access token expiration check is enabled by default for the Google provider since Grafana v10.1.0 if the `accessTokenExpirationCheck` feature toggle is enabled. If you would like to disable access token expiration check then set the `use_refresh_token` configuration value to `false`.
> **Note:** The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.2.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check.
### Configure automatic login
Set `auto_login` option to true to attempt login automatically, skipping the login screen.

View File

@ -75,7 +75,10 @@ When a user logs in using an OAuth provider, Grafana verifies that the access to
Grafana uses a refresh token to obtain a new access token without requiring the user to log in again. If a refresh token doesn't exist, Grafana logs the user out of the system after the access token has expired.
1. To enable the `Refresh Token`, grant type in the `General Settings` section.
1. Extend the `scopes` in `[auth.okta]` with `offline_access`.
1. Extend the `scopes` in `[auth.okta]` with `offline_access` for Grafana versions between v9.3 and v10.0.x.
1. Set `use_refresh_token` in `[auth.okta]` to `true` for Grafana versions v10.1.0 and later.
> **Note:** The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.2.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check.
### Configure allowed groups and domains

View File

@ -11,8 +11,6 @@ import (
"strconv"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
type SocialGenericOAuth struct {
@ -489,13 +487,6 @@ func (s *SocialGenericOAuth) FetchOrganizations(ctx context.Context, client *htt
return logins, true
}
func (s *SocialGenericOAuth) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
opts = append(opts, oauth2.AccessTypeOffline)
}
return s.SocialBase.AuthCodeURL(state, opts...)
}
func (s *SocialGenericOAuth) SupportBundleContent(bf *bytes.Buffer) error {
bf.WriteString("## GenericOAuth specific configuration\n\n")
bf.WriteString("```ini\n")

View File

@ -114,7 +114,7 @@ func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client)
}
func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) && s.useRefreshToken {
opts = append(opts, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
}
return s.SocialBase.AuthCodeURL(state, opts...)

View File

@ -17,6 +17,7 @@ import (
"strings"
"time"
"golang.org/x/exp/slices"
"golang.org/x/oauth2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
@ -31,6 +32,10 @@ import (
"github.com/grafana/grafana/pkg/util"
)
const (
OfflineAccessScope = "offline_access"
)
type SocialService struct {
cfg *setting.Cfg
@ -66,6 +71,7 @@ type OAuthInfo struct {
RoleAttributeStrict bool `toml:"role_attribute_strict"`
TlsSkipVerify bool `toml:"tls_skip_verify"`
UsePKCE bool `toml:"use_pkce"`
UseRefreshToken bool `toml:"use_refresh_token"`
}
func ProvideService(cfg *setting.Cfg,
@ -111,6 +117,7 @@ func ProvideService(cfg *setting.Cfg,
TlsClientCa: sec.Key("tls_client_ca").String(),
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
UsePKCE: sec.Key("use_pkce").MustBool(),
UseRefreshToken: sec.Key("use_refresh_token").MustBool(false),
AllowAssignGrafanaAdmin: sec.Key("allow_assign_grafana_admin").MustBool(false),
AutoLogin: sec.Key("auto_login").MustBool(false),
}
@ -198,6 +205,9 @@ func ProvideService(cfg *setting.Cfg,
forceUseGraphAPI: sec.Key("force_use_graph_api").MustBool(false),
skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync,
}
if info.UseRefreshToken && features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
appendUniqueScope(&config, OfflineAccessScope)
}
}
// Okta
@ -208,6 +218,9 @@ func ProvideService(cfg *setting.Cfg,
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
skipOrgRoleSync: cfg.OktaSkipOrgRoleSync,
}
if info.UseRefreshToken && features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
appendUniqueScope(&config, OfflineAccessScope)
}
}
// Generic - Uses the same scheme as GitHub.
@ -271,6 +284,7 @@ func (b *BasicUserInfo) String() string {
b.Id, b.Name, b.Email, b.Login, b.Role, b.Groups)
}
//go:generate mockery --name SocialConnector --structname MockSocialConnector --outpkg socialtest --filename social_connector_mock.go --output ../socialtest/
type SocialConnector interface {
UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error)
IsEmailAllowed(email string) bool
@ -295,6 +309,7 @@ type SocialBase struct {
autoAssignOrgRole string
skipOrgRoleSync bool
features featuremgmt.FeatureManager
useRefreshToken bool
}
type Error struct {
@ -344,6 +359,7 @@ func newSocialBase(name string,
roleAttributeStrict: info.RoleAttributeStrict,
skipOrgRoleSync: skipOrgRoleSync,
features: features,
useRefreshToken: info.UseRefreshToken,
}
}
@ -592,3 +608,9 @@ func (s *SocialBase) retrieveRawIDToken(idToken interface{}) ([]byte, error) {
return rawJSON, nil
}
func appendUniqueScope(config *oauth2.Config, scope string) {
if !slices.Contains(config.Scopes, OfflineAccessScope) {
config.Scopes = append(config.Scopes, OfflineAccessScope)
}
}

View File

@ -0,0 +1,190 @@
// Code generated by mockery v2.27.1. DO NOT EDIT.
package socialtest
import (
bytes "bytes"
context "context"
http "net/http"
mock "github.com/stretchr/testify/mock"
oauth2 "golang.org/x/oauth2"
social "github.com/grafana/grafana/pkg/login/social"
)
// MockSocialConnector is an autogenerated mock type for the SocialConnector type
type MockSocialConnector struct {
mock.Mock
}
// AuthCodeURL provides a mock function with given fields: state, opts
func (_m *MockSocialConnector) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, state)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 string
if rf, ok := ret.Get(0).(func(string, ...oauth2.AuthCodeOption) string); ok {
r0 = rf(state, opts...)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Client provides a mock function with given fields: ctx, t
func (_m *MockSocialConnector) Client(ctx context.Context, t *oauth2.Token) *http.Client {
ret := _m.Called(ctx, t)
var r0 *http.Client
if rf, ok := ret.Get(0).(func(context.Context, *oauth2.Token) *http.Client); ok {
r0 = rf(ctx, t)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*http.Client)
}
}
return r0
}
// Exchange provides a mock function with given fields: ctx, code, authOptions
func (_m *MockSocialConnector) Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
_va := make([]interface{}, len(authOptions))
for _i := range authOptions {
_va[_i] = authOptions[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, code)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *oauth2.Token
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error)); ok {
return rf(ctx, code, authOptions...)
}
if rf, ok := ret.Get(0).(func(context.Context, string, ...oauth2.AuthCodeOption) *oauth2.Token); ok {
r0 = rf(ctx, code, authOptions...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*oauth2.Token)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, ...oauth2.AuthCodeOption) error); ok {
r1 = rf(ctx, code, authOptions...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// IsEmailAllowed provides a mock function with given fields: email
func (_m *MockSocialConnector) IsEmailAllowed(email string) bool {
ret := _m.Called(email)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(email)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// IsSignupAllowed provides a mock function with given fields:
func (_m *MockSocialConnector) IsSignupAllowed() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// SupportBundleContent provides a mock function with given fields: _a0
func (_m *MockSocialConnector) SupportBundleContent(_a0 *bytes.Buffer) error {
ret := _m.Called(_a0)
var r0 error
if rf, ok := ret.Get(0).(func(*bytes.Buffer) error); ok {
r0 = rf(_a0)
} else {
r0 = ret.Error(0)
}
return r0
}
// TokenSource provides a mock function with given fields: ctx, t
func (_m *MockSocialConnector) TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource {
ret := _m.Called(ctx, t)
var r0 oauth2.TokenSource
if rf, ok := ret.Get(0).(func(context.Context, *oauth2.Token) oauth2.TokenSource); ok {
r0 = rf(ctx, t)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(oauth2.TokenSource)
}
}
return r0
}
// UserInfo provides a mock function with given fields: ctx, client, token
func (_m *MockSocialConnector) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) {
ret := _m.Called(ctx, client, token)
var r0 *social.BasicUserInfo
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *http.Client, *oauth2.Token) (*social.BasicUserInfo, error)); ok {
return rf(ctx, client, token)
}
if rf, ok := ret.Get(0).(func(context.Context, *http.Client, *oauth2.Token) *social.BasicUserInfo); ok {
r0 = rf(ctx, client, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*social.BasicUserInfo)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *http.Client, *oauth2.Token) error); ok {
r1 = rf(ctx, client, token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewMockSocialConnector interface {
mock.TestingT
Cleanup(func())
}
// NewMockSocialConnector creates a new instance of MockSocialConnector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockSocialConnector(t mockConstructorTestingTNewMockSocialConnector) *MockSocialConnector {
mock := &MockSocialConnector{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,33 @@
package socialtest
import (
"net/http"
"github.com/grafana/grafana/pkg/login/social"
)
type FakeSocialService struct {
ExpectedAuthInfoProvider *social.OAuthInfo
ExpectedConnector social.SocialConnector
ExpectedHttpClient *http.Client
}
func (fss *FakeSocialService) GetOAuthProviders() map[string]bool {
panic("not implemented")
}
func (fss *FakeSocialService) GetOAuthHttpClient(string) (*http.Client, error) {
return fss.ExpectedHttpClient, nil
}
func (fss *FakeSocialService) GetConnector(string) (social.SocialConnector, error) {
return fss.ExpectedConnector, nil
}
func (fss *FakeSocialService) GetOAuthInfoProvider(string) *social.OAuthInfo {
return fss.ExpectedAuthInfoProvider
}
func (fss *FakeSocialService) GetOAuthInfoProviders() map[string]*social.OAuthInfo {
panic("not implemented")
}

View File

@ -158,7 +158,7 @@ func ProvideService(
s.RegisterPostAuthHook(userSyncService.SyncLastSeenHook, 40)
if features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
s.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService).SyncOauthTokenHook, 60)
s.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService).SyncOauthTokenHook, 60)
}
s.RegisterPostAuthHook(userSyncService.FetchSyncedUserHook, 100)

View File

@ -3,10 +3,12 @@ package sync
import (
"context"
"errors"
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"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/oauthtoken"
@ -15,15 +17,19 @@ import (
)
var (
errExpiredAccessToken = errutil.NewBase(errutil.StatusUnauthorized, "oauth.expired-token")
errExpiredAccessToken = errutil.NewBase(
errutil.StatusUnauthorized,
"oauth.expired-token",
errutil.WithPublicMessage("OAuth access token expired"))
)
func ProvideOAuthTokenSync(service oauthtoken.OAuthTokenService, sessionService auth.UserTokenService) *OAuthTokenSync {
func ProvideOAuthTokenSync(service oauthtoken.OAuthTokenService, sessionService auth.UserTokenService, socialService social.Service) *OAuthTokenSync {
return &OAuthTokenSync{
log.New("oauth_token.sync"),
localcache.New(maxOAuthTokenCacheTTL, 15*time.Minute),
service,
sessionService,
socialService,
}
}
@ -32,6 +38,7 @@ type OAuthTokenSync struct {
cache *localcache.CacheService
service oauthtoken.OAuthTokenService
sessionService auth.UserTokenService
socialService social.Service
}
func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error {
@ -64,6 +71,19 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, identity *authn
return nil
}
// get the token's auth provider (f.e. azuread)
provider := strings.TrimPrefix(token.AuthModule, "oauth_")
currentOAuthInfo := s.socialService.GetOAuthInfoProvider(provider)
if currentOAuthInfo == nil {
s.log.Warn("OAuth provider not found", "provider", provider)
return nil
}
// if refresh token handling is disabled for this provider, we can skip the hook
if !currentOAuthInfo.UseRefreshToken {
return nil
}
expires := token.OAuthExpiry.Round(0).Add(-oauthtoken.ExpiryDelta)
// token has not expired, so we don't have to refresh it
if !expires.Before(time.Now()) {
@ -78,14 +98,14 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, identity *authn
}
if err := s.service.InvalidateOAuthTokens(ctx, token); err != nil {
s.log.FromContext(ctx).Error("Failed invalidate OAuth tokens", "id", identity.ID, "error", err)
s.log.FromContext(ctx).Error("Failed to invalidate OAuth tokens", "id", identity.ID, "error", err)
}
if err := s.sessionService.RevokeToken(ctx, identity.SessionToken, false); err != nil {
s.log.FromContext(ctx).Error("Failed to revoke session token", "id", identity.ID, "tokenId", identity.SessionToken.Id, "error", err)
}
return errExpiredAccessToken.Errorf("oauth access token could not be refreshed: %w", auth.ErrInvalidSessionToken)
return errExpiredAccessToken.Errorf("oauth access token could not be refreshed: %w", err)
}
return nil

View File

@ -10,6 +10,8 @@ import (
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/socialtest"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/authn"
@ -20,8 +22,9 @@ import (
func TestOauthTokenSync_SyncOAuthTokenHook(t *testing.T) {
type testCase struct {
desc string
identity *authn.Identity
desc string
identity *authn.Identity
oauthInfo *social.OAuthInfo
expectedHasEntryToken *login.UserAuth
expectHasEntryCalled bool
@ -84,6 +87,13 @@ func TestOauthTokenSync_SyncOAuthTokenHook(t *testing.T) {
expectRevokeTokenCalled: true,
expectedHasEntryToken: &login.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)},
expectedErr: errExpiredAccessToken,
}, {
desc: "should skip sync when use_refresh_token is disabled",
identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}, AuthenticatedBy: login.GitLabAuthModule},
expectHasEntryCalled: true,
expectTryRefreshTokenCalled: false,
expectedHasEntryToken: &login.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)},
oauthInfo: &social.OAuthInfo{UseRefreshToken: false},
},
}
@ -118,11 +128,22 @@ func TestOauthTokenSync_SyncOAuthTokenHook(t *testing.T) {
},
}
if tt.oauthInfo == nil {
tt.oauthInfo = &social.OAuthInfo{
UseRefreshToken: true,
}
}
socialService := &socialtest.FakeSocialService{
ExpectedAuthInfoProvider: tt.oauthInfo,
}
sync := &OAuthTokenSync{
log: log.NewNopLogger(),
cache: localcache.New(0, 0),
service: service,
sessionService: sessionService,
socialService: socialService,
}
err := sync.SyncOauthTokenHook(context.Background(), tt.identity, nil)

View File

@ -1,10 +1,8 @@
package oauthtoken
import (
"bytes"
"context"
"errors"
"net/http"
"reflect"
"testing"
"time"
@ -15,7 +13,7 @@ import (
"golang.org/x/sync/singleflight"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/socialtest"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
"github.com/grafana/grafana/pkg/services/user"
@ -221,12 +219,12 @@ func TestService_TryTokenRefresh_DifferentAuthModuleForUser(t *testing.T) {
socialConnector.AssertNotCalled(t, "TokenSource")
}
func setupOAuthTokenService(t *testing.T) (*Service, *FakeAuthInfoStore, *MockSocialConnector) {
func setupOAuthTokenService(t *testing.T) (*Service, *FakeAuthInfoStore, *socialtest.MockSocialConnector) {
t.Helper()
socialConnector := &MockSocialConnector{}
socialService := &FakeSocialService{
connector: socialConnector,
socialConnector := &socialtest.MockSocialConnector{}
socialService := &socialtest.FakeSocialService{
ExpectedConnector: socialConnector,
}
authInfoStore := &FakeAuthInfoStore{}
@ -239,74 +237,6 @@ func setupOAuthTokenService(t *testing.T) (*Service, *FakeAuthInfoStore, *MockSo
}, authInfoStore, socialConnector
}
type FakeSocialService struct {
httpClient *http.Client
connector *MockSocialConnector
}
func (fss *FakeSocialService) GetOAuthProviders() map[string]bool {
panic("not implemented")
}
func (fss *FakeSocialService) GetOAuthHttpClient(string) (*http.Client, error) {
return fss.httpClient, nil
}
func (fss *FakeSocialService) GetConnector(string) (social.SocialConnector, error) {
return fss.connector, nil
}
func (fss *FakeSocialService) GetOAuthInfoProvider(string) *social.OAuthInfo {
panic("not implemented")
}
func (fss *FakeSocialService) GetOAuthInfoProviders() map[string]*social.OAuthInfo {
panic("not implemented")
}
type MockSocialConnector struct {
mock.Mock
}
func (m *MockSocialConnector) Type() int {
args := m.Called()
return args.Int(0)
}
func (m *MockSocialConnector) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) {
args := m.Called(client, token)
return args.Get(0).(*social.BasicUserInfo), args.Error(1)
}
func (m *MockSocialConnector) IsEmailAllowed(email string) bool {
panic("not implemented")
}
func (m *MockSocialConnector) IsSignupAllowed() bool {
panic("not implemented")
}
func (m *MockSocialConnector) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
panic("not implemented")
}
func (m *MockSocialConnector) Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
panic("not implemented")
}
func (m *MockSocialConnector) Client(ctx context.Context, t *oauth2.Token) *http.Client {
panic("not implemented")
}
func (m *MockSocialConnector) TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource {
args := m.Called(ctx, t)
return args.Get(0).(oauth2.TokenSource)
}
func (m *MockSocialConnector) SupportBundleContent(bf *bytes.Buffer) error {
return nil
}
type FakeAuthInfoStore struct {
login.Store
ExpectedError error