SSO: Add prompt param to SSO settings (#107969)

* add prompt param to AzureAD oauth config

* yarn i18n-extract

* validate auth prompt value

* make login_prompt available for all SSO providers

* use base authCodeURL for azure and google

* add docs for the new field for azure and generic oauth

* fix typo

* fix frontend unit test

* add prompt parameter to docs for the other providers

* remove prompt from okta

* add unit tests for the other providers

* address feedback

* add back translations for prompt labels
This commit is contained in:
Mihai Doarna
2025-07-17 14:40:48 +03:00
committed by GitHub
parent 807264428e
commit 8dfb4cdfc9
22 changed files with 169 additions and 6 deletions
+1 -1
View File
@@ -336,7 +336,7 @@ func (s *SocialAzureAD) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption)
opts = append(opts, oauth2.SetAuthURLParam("domain_hint", domainHint))
}
return s.Config.AuthCodeURL(state, opts...)
return s.getAuthCodeURL(state, opts...)
}
func (s *SocialAzureAD) validateIDTokenSignature(ctx context.Context, client *http.Client, parsedToken *jwt.JSONWebToken) (*azureClaims, error) {
@@ -1276,6 +1276,22 @@ func TestSocialAzureAD_Validate(t *testing.T) {
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if login prompt is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_authentication": "client_secret_post",
"client_id": "client-id",
"client_secret": "client_secret",
"allowed_groups": "0bb9c9cc-4945-418f-9b6a-c1d3b81141b0, 6034d328-0e6a-4240-8d03-cb9f2c1f16e4",
"allow_assign_grafana_admin": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
"login_prompt": "invalid",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
}
for _, tc := range testCases {
@@ -1315,6 +1331,7 @@ func TestSocialAzureAD_Reload(t *testing.T) {
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": "some-new-url",
"login_prompt": "select_account",
},
},
expectError: false,
@@ -1322,6 +1339,7 @@ func TestSocialAzureAD_Reload(t *testing.T) {
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
AuthUrl: "some-new-url",
LoginPrompt: "select_account",
},
expectedConfig: &oauth2.Config{
ClientID: "new-client-id",
@@ -1230,6 +1230,20 @@ func TestSocialGenericOAuth_Validate(t *testing.T) {
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if login prompt is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"teams_url": "https://example.com/teams",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
"login_prompt": "invalid",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
}
for _, tc := range testCases {
@@ -1269,6 +1283,7 @@ func TestSocialGenericOAuth_Reload(t *testing.T) {
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": "some-new-url",
"login_prompt": "login",
},
},
expectError: false,
@@ -1276,6 +1291,7 @@ func TestSocialGenericOAuth_Reload(t *testing.T) {
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
AuthUrl: "some-new-url",
LoginPrompt: "login",
},
expectedConfig: &oauth2.Config{
ClientID: "new-client-id",
@@ -1357,6 +1373,7 @@ func TestGenericOAuth_Reload_ExtraFields(t *testing.T) {
EmailAttributeName: "email-attr-name",
GroupsAttributePath: "groups-attr-path",
TeamIdsAttributePath: "team-ids-attr-path",
LoginPrompt: "login",
Extra: map[string]string{
teamIdsKey: "team1",
allowedOrganizationsKey: "org1",
@@ -1374,6 +1391,7 @@ func TestGenericOAuth_Reload_ExtraFields(t *testing.T) {
"email_attribute_name": "new-email-attr-name",
"groups_attribute_path": "new-group-attr-path",
"team_ids_attribute_path": "new-team-ids-attr-path",
"login_prompt": "select_account",
teamIdsKey: "team1,team2",
allowedOrganizationsKey: "org1,org2",
loginAttributePathKey: "new-login-attr-path",
@@ -1389,6 +1407,7 @@ func TestGenericOAuth_Reload_ExtraFields(t *testing.T) {
EmailAttributeName: "new-email-attr-name",
GroupsAttributePath: "new-group-attr-path",
TeamIdsAttributePath: "new-team-ids-attr-path",
LoginPrompt: "select_account",
Extra: map[string]string{
teamIdsKey: "team1,team2",
allowedOrganizationsKey: "org1,org2",
@@ -495,6 +495,7 @@ func TestSocialGitHub_Validate(t *testing.T) {
"auth_url": "",
"token_url": "",
"api_url": "",
"login_prompt": "select_account",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
@@ -594,6 +595,17 @@ func TestSocialGitHub_Validate(t *testing.T) {
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if login prompt is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"login_prompt": "invalid",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
}
for _, tc := range testCases {
@@ -634,6 +646,7 @@ func TestSocialGitHub_Reload(t *testing.T) {
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": "some-new-url",
"login_prompt": "login",
},
},
expectError: false,
@@ -641,6 +654,7 @@ func TestSocialGitHub_Reload(t *testing.T) {
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
AuthUrl: "some-new-url",
LoginPrompt: "login",
},
expectedConfig: &oauth2.Config{
ClientID: "new-client-id",
@@ -547,6 +547,7 @@ func TestSocialGitlab_Validate(t *testing.T) {
"auth_url": "",
"token_url": "",
"api_url": "",
"login_prompt": "select_account",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
@@ -640,6 +641,17 @@ func TestSocialGitlab_Validate(t *testing.T) {
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if login prompt is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"login_prompt": "invalid",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
}
for _, tc := range testCases {
@@ -680,6 +692,7 @@ func TestSocialGitlab_Reload(t *testing.T) {
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": "some-new-url",
"login_prompt": "login",
},
},
expectError: false,
@@ -687,6 +700,7 @@ func TestSocialGitlab_Reload(t *testing.T) {
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
AuthUrl: "some-new-url",
LoginPrompt: "login",
},
expectedConfig: &oauth2.Config{
ClientID: "new-client-id",
+2 -1
View File
@@ -224,7 +224,8 @@ func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption)
if s.info.UseRefreshToken {
opts = append(opts, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
}
return s.Config.AuthCodeURL(state, opts...)
return s.getAuthCodeURL(state, opts...)
}
func (s *SocialGoogle) extractFromToken(_ context.Context, _ *http.Client, token *oauth2.Token) (*googleUserData, error) {
@@ -724,6 +724,7 @@ func TestSocialGoogle_Validate(t *testing.T) {
"auth_url": "",
"token_url": "",
"api_url": "",
"login_prompt": "select_account",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
@@ -830,6 +831,17 @@ func TestSocialGoogle_Validate(t *testing.T) {
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if login prompt is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"login_prompt": "invalid",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
}
for _, tc := range testCases {
@@ -870,6 +882,7 @@ func TestSocialGoogle_Reload(t *testing.T) {
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"auth_url": "some-new-url",
"login_prompt": "login",
},
},
expectError: false,
@@ -877,6 +890,7 @@ func TestSocialGoogle_Reload(t *testing.T) {
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
AuthUrl: "some-new-url",
LoginPrompt: "login",
},
expectedConfig: &oauth2.Config{
ClientID: "new-client-id",
+12 -1
View File
@@ -85,6 +85,15 @@ func (s *SocialBase) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) st
s.reloadMutex.RLock()
defer s.reloadMutex.RUnlock()
return s.getAuthCodeURL(state, opts...)
}
func (s *SocialBase) getAuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
if s.info.LoginPrompt != "" {
promptOpt := oauth2.SetAuthURLParam("prompt", s.info.LoginPrompt)
opts = append(opts, promptOpt)
}
return s.Config.AuthCodeURL(state, opts...)
}
@@ -268,5 +277,7 @@ func validateInfo(info *social.OAuthInfo, oldInfo *social.OAuthInfo, requester i
validation.AllowAssignGrafanaAdminValidator(info, oldInfo, requester),
validation.SkipOrgRoleSyncAllowAssignGrafanaAdminValidator,
validation.OrgAttributePathValidator(info, oldInfo, requester),
validation.OrgMappingValidator(info, oldInfo, requester))
validation.OrgMappingValidator(info, oldInfo, requester),
validation.LoginPromptValidator,
)
}
+1
View File
@@ -99,6 +99,7 @@ type OAuthInfo struct {
TokenUrl string `mapstructure:"token_url" toml:"token_url"`
UsePKCE bool `mapstructure:"use_pkce" toml:"use_pkce"`
UseRefreshToken bool `mapstructure:"use_refresh_token" toml:"use_refresh_token"`
LoginPrompt string `mapstructure:"login_prompt" toml:"login_prompt"`
Extra map[string]string `mapstructure:",remain" toml:"extra,omitempty"`
}
@@ -108,6 +108,7 @@ func (s *OAuthStrategy) loadSettingsForProvider(provider string) map[string]any
"signout_redirect_url": section.Key("signout_redirect_url").Value(),
"org_mapping": section.Key("org_mapping").Value(),
"org_attribute_path": section.Key("org_attribute_path").Value(),
"login_prompt": section.Key("login_prompt").Value(),
}
extraKeys := extraKeysByProvider[provider]
@@ -58,6 +58,7 @@ var (
signout_redirect_url = test_signout_redirect_url
org_attribute_path = groups
org_mapping = Group1:*:Editor
login_prompt = select_account
`
expectedOAuthInfo = map[string]any{
@@ -104,6 +105,7 @@ var (
"team_ids": "first, second",
"org_attribute_path": "groups",
"org_mapping": "Group1:*:Editor",
"login_prompt": "select_account",
}
)
@@ -51,6 +51,15 @@ func SkipOrgRoleSyncAllowAssignGrafanaAdminValidator(info *social.OAuthInfo, req
return nil
}
func LoginPromptValidator(info *social.OAuthInfo, requester identity.Requester) error {
prompt := info.LoginPrompt
if prompt != "" && prompt != "login" && prompt != "consent" && prompt != "select_account" {
return ssosettings.ErrInvalidOAuthConfig("Invalid value for login_prompt. Valid values are: login, consent, select_account.")
}
return nil
}
func RequiredValidator(value string, name string) ssosettings.ValidateFunc[social.OAuthInfo] {
return func(info *social.OAuthInfo, requester identity.Requester) error {
if value == "" {