OAuth: Make sub claim required for generic oauth behind feature toggle (#85065)

* Add feature toggle for sub claims requirement

* OAuth: require valid auth id

* Fix feature toggle description
This commit is contained in:
Karl Persson
2024-03-25 14:22:24 +01:00
committed by GitHub
parent e2f155f9f7
commit 2f3a01f79f
8 changed files with 103 additions and 10 deletions

View File

@@ -176,4 +176,5 @@ export interface FeatureToggles {
scopeFilters?: boolean; scopeFilters?: boolean;
ssoSettingsSAML?: boolean; ssoSettingsSAML?: boolean;
usePrometheusFrontendPackage?: boolean; usePrometheusFrontendPackage?: boolean;
oauthRequireSubClaim?: boolean;
} }

View File

@@ -142,7 +142,7 @@ func ProvideService(
for name := range socialService.GetOAuthProviders() { for name := range socialService.GetOAuthProviders() {
clientName := authn.ClientWithPrefix(name) clientName := authn.ClientWithPrefix(name)
s.RegisterClient(clients.ProvideOAuth(clientName, cfg, oauthTokenService, socialService, settingsProviderService)) s.RegisterClient(clients.ProvideOAuth(clientName, cfg, oauthTokenService, socialService, settingsProviderService, features))
} }
// FIXME (jguer): move to User package // FIXME (jguer): move to User package

View File

@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/login/social/connectors" "github.com/grafana/grafana/pkg/login/social/connectors"
"github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/authn" "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"
"github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
@@ -65,12 +66,12 @@ var _ authn.RedirectClient = new(OAuth)
func ProvideOAuth( func ProvideOAuth(
name string, cfg *setting.Cfg, oauthService oauthtoken.OAuthTokenService, name string, cfg *setting.Cfg, oauthService oauthtoken.OAuthTokenService,
socialService social.Service, settingsProviderService setting.Provider, socialService social.Service, settingsProviderService setting.Provider, features featuremgmt.FeatureToggles,
) *OAuth { ) *OAuth {
providerName := strings.TrimPrefix(name, "auth.client.") providerName := strings.TrimPrefix(name, "auth.client.")
return &OAuth{ return &OAuth{
name, fmt.Sprintf("oauth_%s", providerName), providerName, name, fmt.Sprintf("oauth_%s", providerName), providerName,
log.New(name), cfg, settingsProviderService, oauthService, socialService, log.New(name), cfg, settingsProviderService, oauthService, socialService, features,
} }
} }
@@ -84,6 +85,7 @@ type OAuth struct {
settingsProviderSvc setting.Provider settingsProviderSvc setting.Provider
oauthService oauthtoken.OAuthTokenService oauthService oauthtoken.OAuthTokenService
socialService social.Service socialService social.Service
features featuremgmt.FeatureToggles
} }
func (c *OAuth) Name() string { func (c *OAuth) Name() string {
@@ -148,10 +150,13 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
return nil, errOAuthUserInfo.Errorf("failed to get user info: %w", err) return nil, errOAuthUserInfo.Errorf("failed to get user info: %w", err)
} }
// Implement in Grafana 11 if userInfo.Id == "" {
// if userInfo.Id == "" { if c.features.IsEnabledGlobally(featuremgmt.FlagOauthRequireSubClaim) {
// return nil, errors.New("idP did not return a user id") return nil, errOAuthUserInfo.Errorf("missing required sub claims")
// } } else {
c.log.FromContext(ctx).Warn("Missing sub claim, oauth authentication without a sub claim is deprecated and will be rejected in future versions.")
}
}
if userInfo.Email == "" { if userInfo.Email == "" {
return nil, errOAuthMissingRequiredEmail.Errorf("required attribute email was not provided") return nil, errOAuthMissingRequiredEmail.Errorf("required attribute email was not provided")

View File

@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/login/social/socialtest" "github.com/grafana/grafana/pkg/login/social/socialtest"
"github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/authn" "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"
"github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
@@ -37,6 +38,8 @@ func TestOAuth_Authenticate(t *testing.T) {
addPKCECookie bool addPKCECookie bool
pkceCookieValue string pkceCookieValue string
features []any
isEmailAllowed bool isEmailAllowed bool
userInfo *social.BasicUserInfo userInfo *social.BasicUserInfo
@@ -120,6 +123,24 @@ func TestOAuth_Authenticate(t *testing.T) {
isEmailAllowed: false, isEmailAllowed: false,
expectedErr: errOAuthEmailNotAllowed, expectedErr: errOAuthEmailNotAllowed,
}, },
{
desc: "should return error when no auth id is set and feature toggle is enabled",
req: &authn.Request{
HTTPRequest: &http.Request{
Header: map[string][]string{},
URL: mustParseURL("http://grafana.com/?state=some-state"),
},
},
features: []any{featuremgmt.FlagOauthRequireSubClaim},
oauthCfg: &social.OAuthInfo{UsePKCE: true, Enabled: true},
addStateCookie: true,
stateCookieValue: "some-state",
addPKCECookie: true,
pkceCookieValue: "some-pkce-value",
userInfo: &social.BasicUserInfo{Email: "some@email.com"},
isEmailAllowed: false,
expectedErr: errOAuthUserInfo,
},
{ {
desc: "should return identity for valid request", desc: "should return identity for valid request",
req: &authn.Request{HTTPRequest: &http.Request{ req: &authn.Request{HTTPRequest: &http.Request{
@@ -197,6 +218,42 @@ func TestOAuth_Authenticate(t *testing.T) {
}, },
}, },
}, },
{
desc: "should return identity when feature toggle is enabled and auth id is set",
req: &authn.Request{
HTTPRequest: &http.Request{
Header: map[string][]string{},
URL: mustParseURL("http://grafana.com/?state=some-state"),
},
},
oauthCfg: &social.OAuthInfo{Enabled: true},
addStateCookie: true,
stateCookieValue: "some-state",
isEmailAllowed: true,
features: []any{featuremgmt.FlagOauthRequireSubClaim},
userInfo: &social.BasicUserInfo{
Id: "123",
Name: "name",
Email: "some@email.com",
Role: "Admin",
},
expectedIdentity: &authn.Identity{
Email: "some@email.com",
AuthenticatedBy: login.AzureADAuthModule,
AuthID: "123",
Name: "name",
OAuthToken: &oauth2.Token{},
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeams: true,
AllowSignUp: true,
FetchSyncedUser: true,
SyncOrgRoles: true,
LookUpParams: login.UserLookupParams{},
},
},
},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -231,7 +288,7 @@ func TestOAuth_Authenticate(t *testing.T) {
}, },
} }
c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, nil, fakeSocialSvc, settingsProvider) c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, nil, fakeSocialSvc, settingsProvider, featuremgmt.WithFeatures(tt.features...))
identity, err := c.Authenticate(context.Background(), tt.req) identity, err := c.Authenticate(context.Background(), tt.req)
assert.ErrorIs(t, err, tt.expectedErr) assert.ErrorIs(t, err, tt.expectedErr)
@@ -314,7 +371,7 @@ func TestOAuth_RedirectURL(t *testing.T) {
cfg := setting.NewCfg() cfg := setting.NewCfg()
c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, nil, fakeSocialSvc, &setting.OSSImpl{Cfg: cfg}) c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, nil, fakeSocialSvc, &setting.OSSImpl{Cfg: cfg}, featuremgmt.WithFeatures())
redirect, err := c.RedirectURL(context.Background(), nil) redirect, err := c.RedirectURL(context.Background(), nil)
assert.ErrorIs(t, err, tt.expectedErr) assert.ErrorIs(t, err, tt.expectedErr)
@@ -427,7 +484,7 @@ func TestOAuth_Logout(t *testing.T) {
fakeSocialSvc := &socialtest.FakeSocialService{ fakeSocialSvc := &socialtest.FakeSocialService{
ExpectedAuthInfoProvider: tt.oauthCfg, ExpectedAuthInfoProvider: tt.oauthCfg,
} }
c := ProvideOAuth(authn.ClientWithPrefix("azuread"), tt.cfg, mockService, fakeSocialSvc, &setting.OSSImpl{Cfg: tt.cfg}) c := ProvideOAuth(authn.ClientWithPrefix("azuread"), tt.cfg, mockService, fakeSocialSvc, &setting.OSSImpl{Cfg: tt.cfg}, featuremgmt.WithFeatures())
redirect, ok := c.Logout(context.Background(), &authn.Identity{}, &login.UserAuth{}) redirect, ok := c.Logout(context.Background(), &authn.Identity{}, &login.UserAuth{})

View File

@@ -1181,6 +1181,14 @@ var (
FrontendOnly: true, FrontendOnly: true,
Owner: grafanaObservabilityMetricsSquad, Owner: grafanaObservabilityMetricsSquad,
}, },
{
Name: "oauthRequireSubClaim",
Description: "Require that sub claims is present in oauth tokens.",
Stage: FeatureStageExperimental,
Owner: identityAccessTeam,
HideFromDocs: true,
HideFromAdminPage: true,
},
} }
) )

View File

@@ -157,3 +157,4 @@ betterPageScrolling,GA,@grafana/grafana-frontend-platform,false,false,true
scopeFilters,experimental,@grafana/dashboards-squad,false,false,false scopeFilters,experimental,@grafana/dashboards-squad,false,false,false
ssoSettingsSAML,experimental,@grafana/identity-access-team,false,false,false ssoSettingsSAML,experimental,@grafana/identity-access-team,false,false,false
usePrometheusFrontendPackage,experimental,@grafana/observability-metrics,false,false,true usePrometheusFrontendPackage,experimental,@grafana/observability-metrics,false,false,true
oauthRequireSubClaim,experimental,@grafana/identity-access-team,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
157 scopeFilters experimental @grafana/dashboards-squad false false false
158 ssoSettingsSAML experimental @grafana/identity-access-team false false false
159 usePrometheusFrontendPackage experimental @grafana/observability-metrics false false true
160 oauthRequireSubClaim experimental @grafana/identity-access-team false false false

View File

@@ -638,4 +638,8 @@ const (
// FlagUsePrometheusFrontendPackage // FlagUsePrometheusFrontendPackage
// Use the @grafana/prometheus frontend package in core Prometheus. // Use the @grafana/prometheus frontend package in core Prometheus.
FlagUsePrometheusFrontendPackage = "usePrometheusFrontendPackage" FlagUsePrometheusFrontendPackage = "usePrometheusFrontendPackage"
// FlagOauthRequireSubClaim
// Require that sub claims is present in oauth tokens.
FlagOauthRequireSubClaim = "oauthRequireSubClaim"
) )

View File

@@ -2046,6 +2046,23 @@
"stage": "experimental", "stage": "experimental",
"codeowner": "@grafana/alerting-squad" "codeowner": "@grafana/alerting-squad"
} }
},
{
"metadata": {
"name": "oauthRequireSubClaim",
"resourceVersion": "1711371458317",
"creationTimestamp": "2024-03-25T10:50:29Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-03-25 12:57:38.317427693 +0000 UTC"
}
},
"spec": {
"description": "Require that sub claims is present in oauth tokens.",
"stage": "experimental",
"codeowner": "@grafana/identity-access-team",
"hideFromAdminPage": true,
"hideFromDocs": true
}
} }
] ]
} }