From 79d565f285343bbf2898884835e4f45cfb9c6ee3 Mon Sep 17 00:00:00 2001 From: John Naizer Date: Tue, 7 Jan 2025 01:42:52 -0800 Subject: [PATCH] OAuth: Support client_secret_jwt for oauth providers when doing token exchange (#95455) * added backend support for client_secret_jwt * added backend support for client_secret_jwt * added all logic to the exchange function (overloaded social exchange in azuread_oauth to handle managed identity client id) * ran yarn install to update lock file * added support for client_secret_jwt when managed_identity_client_id is null * added audience flag and changed exchange to directly access oauth config using .info * added logic in setting oauth.Config for supported client authentication values * added client_authentication, managed_identity_client_id, and audience to sample.ini file * using provided ctx in ManagedIdentityCallback function * added frontend support for federated identity credential auth * added client authentication field * added Azure AD documentation for Grafana * added bold font to "Add" keyword in documentation * minor wording change relating to previous commit * addressed changing audience to federated_credential_audience, moving validation, and changing managedIdentityCallback to private function * correction to audience name changing * fixed orgMappingClientAuthentication function name, and added in logic into validateFederatedCredentialAudience function * Change docs * Add iam team as owner of azcore pkg * added backend support for client_secret_jwt * added all logic to the exchange function (overloaded social exchange in azuread_oauth to handle managed identity client id) * ran yarn install to update lock file * added support for client_secret_jwt when managed_identity_client_id is null * added audience flag and changed exchange to directly access oauth config using .info * added logic in setting oauth.Config for supported client authentication values * added client_authentication, managed_identity_client_id, and audience to sample.ini file * using provided ctx in ManagedIdentityCallback function * added frontend support for federated identity credential auth * added client authentication field * added Azure AD documentation for Grafana * added bold font to "Add" keyword in documentation * minor wording change relating to previous commit * addressed changing audience to federated_credential_audience, moving validation, and changing managedIdentityCallback to private function * correction to audience name changing * fixed orgMappingClientAuthentication function name, and added in logic into validateFederatedCredentialAudience function * Change docs * Add iam team as owner of azcore pkg * updated yarn lock file * updated doc for correction * removed wrong changes in pkg directory * removed newline in dashboard-generate.yaml and unified.ts * updated yarn.lock to match upstream * Lint Signed-off-by: Jack Baldry * removing unwanted changes * added back removed newline * fixed failing test in azuread_oauth_test.go * Update azuread_oauth.go removed unnecessary newline, fixed lint --------- Signed-off-by: Jack Baldry Co-authored-by: Mihaly Gyongyosi Co-authored-by: Jack Baldry --- conf/defaults.ini | 3 + conf/sample.ini | 3 + .../configure-authentication/azuread/index.md | 79 +++++++++---- go.mod | 2 +- pkg/login/social/connectors/azuread_oauth.go | 104 ++++++++++++++++++ .../social/connectors/azuread_oauth_test.go | 18 +++ pkg/login/social/connectors/common.go | 12 +- pkg/login/social/connectors/social_base.go | 3 + pkg/login/social/social.go | 79 +++++++------ pkg/login/social/socialimpl/service_test.go | 70 ++++++------ .../ssosettings/strategies/oauth_strategy.go | 71 ++++++------ .../strategies/oauth_strategy_test.go | 84 +++++++------- public/app/features/auth-config/fields.tsx | 39 +++++++ public/app/features/auth-config/types.ts | 3 + 14 files changed, 407 insertions(+), 163 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index c0ec909848f..9c74b4c2197 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -779,8 +779,11 @@ icon = microsoft enabled = false allow_sign_up = true auto_login = false +client_authentication = client_id = some_client_id client_secret = +managed_identity_client_id = +federated_credential_audience = scopes = openid email profile auth_url = https://login.microsoftonline.com//oauth2/v2.0/authorize token_url = https://login.microsoftonline.com//oauth2/v2.0/token diff --git a/conf/sample.ini b/conf/sample.ini index 5e09cbcf2c6..705b06e2195 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -758,8 +758,11 @@ ;enabled = false ;allow_sign_up = true ;auto_login = false +;client_authentication = ;client_id = some_client_id ;client_secret = some_client_secret +;managed_identity_client_id = +;federated_credential_audience = ;scopes = openid email profile ;auth_url = https://login.microsoftonline.com//oauth2/v2.0/authorize ;token_url = https://login.microsoftonline.com//oauth2/v2.0/token diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index d839ec7df4a..8ac7d9bd79f 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -46,16 +46,44 @@ To enable the Azure AD/Entra ID OAuth, register your application with Entra ID. - Note the **OAuth 2.0 authorization endpoint (v2)** URL. This is the authorization URL. - Note the **OAuth 2.0 token endpoint (v2)**. This is the token URL. -1. Click **Certificates & secrets** in the side menu, then add a new entry under **Client secrets** with the following configuration. +1. Click **Certificates & secrets** in the side menu, then add a new entry under the supported client authentication option you want to use. The following are the supported client authentication options with their respective configuration steps. - - Description: Grafana OAuth - - Expires: Select an expiration period + - **Client secrets** -1. Click **Add** then copy the key **Value**. This is the OAuth client secret. + 1. Add a new entry under **Client secrets** with the following configuration. -{{% admonition type="note" %}} -Make sure that you copy the string in the **Value** field, rather than the one in the **Secret ID** field. -{{% /admonition %}} + - Description: Grafana OAuth 2.0 + - Expires: Select an expiration period + + 1. Click **Add** then copy the key **Value**. This is the OAuth 2.0 client secret. + + {{< admonition type="note" >}} + Make sure that you copy the string in the **Value** field, rather than the one in the **Secret ID** field. + {{< /admonition >}} + + 1. You must have set `client_authentication` under `[auth.azuread]` to `client_secret_post` in the Grafana server configuration for this to work. + + - **Federated credentials** + + 1. Refer to [Configure an application to trust a managed identity (preview)](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-config-app-trust-managed-identity?tabs=microsoft-entra-admin-center) for a complete guide on setting up a managed identity as a federated credential. + Add a new entry under Federated credentials with the following configuration. + + - Federated credential scenario: Select **Other issuer**. + - Issuer: The OAuth 2.0 / OIDC issuer URL of the Microsoft Entra ID authority. For example: `https://login.microsoftonline.com/{tenantID}/v2.0`. + - Subject identifier: The Object (Principal) ID GUID of the Managed Identity. + - Name: A unique descriptive name for the credential. + - Description: Grafana OAuth. + - Audience: The audience value that must appear in the external token. For Public cloud, it would be `api://AzureADTokenExchange`. See mentioned documentation for the full list of available audiences. + + 1. Click **Add**, and then copy the Managed Identity Client ID and the federated credential Audience values. This is your OAuth 2.0 federated credential. + + 1. You must have set `client_authentication` under `[auth.azuread]` to `managed_identity` in the Grafana server configuration for this to work. + + {{< admonition type="note" >}} + Managed identities as federated credentials are only applicable to workloads hosted in Azure. + + You can only add user-assigned managed identities as federated credentials on Entra ID applications. + {{< /admonition >}} 1. Define the required application roles for Grafana [using the Azure Portal](#configure-application-roles-for-grafana-in-the-azure-portal) or [using the manifest file](#configure-application-roles-for-grafana-in-the-manifest-file). @@ -201,19 +229,22 @@ Available in Public Preview in Grafana 10.4 behind the `ssoSettingsApi` feature resource "grafana_sso_settings" "azuread_sso_settings" { provider_name = "azuread" oauth2_settings { - name = "Azure AD" - auth_url = "https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/authorize" - token_url = "https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token" - client_id = "APPLICATION_ID" - client_secret = "CLIENT_SECRET" - allow_sign_up = true - auto_login = false - scopes = "openid email profile" - allowed_organizations = "TENANT_ID" - role_attribute_strict = false - allow_assign_grafana_admin = false - skip_org_role_sync = false - use_pkce = true + name = "Azure AD" + auth_url = "https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/authorize" + token_url = "https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token" + client_authentication = "CLIENT_AUTHENTICATION_OPTION" + client_id = "APPLICATION_ID" + client_secret = "CLIENT_SECRET" + managed_identity_client_id = "MANAGED_IDENTITY_CLIENT_ID" + federated_credential_audience = "FEDERATED_CREDENTIAL_AUDIENCE" + allow_sign_up = true + auto_login = false + scopes = "openid email profile" + allowed_organizations = "TENANT_ID" + role_attribute_strict = false + allow_assign_grafana_admin = false + skip_org_role_sync = false + use_pkce = true } } ``` @@ -234,8 +265,11 @@ name = Azure AD enabled = true allow_sign_up = true auto_login = false +client_authentication = CLIENT_AUTHENTICATION_OPTION client_id = APPLICATION_ID client_secret = CLIENT_SECRET +managed_identity_client_id = MANAGED_IDENTITY_CLIENT_ID +federated_credential_audience = FEDERATED_CREDENTIAL_AUDIENCE scopes = openid email profile auth_url = https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/authorize token_url = https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token @@ -248,11 +282,14 @@ skip_org_role_sync = false use_pkce = true ``` -You can also use these environment variables to configure **client_id** and **client_secret**: +You can also use these environment variables to configure `client_authentication`, `client_id`, `client_secret`, `managed_identity_client_id`, and `federated_credential_audience`: ``` +GF_AUTH_AZUREAD_CLIENT_AUTHENTICATION GF_AUTH_AZUREAD_CLIENT_ID GF_AUTH_AZUREAD_CLIENT_SECRET +GF_AUTH_AZUREAD_MANAGED_IDENTITY_CLIENT_ID +GF_AUTH_AZUREAD_FEDERATED_CREDENTIAL_AUDIENCE ``` {{% admonition type="note" %}} diff --git a/go.mod b/go.mod index 253356f8911..c9c72bcfffa 100644 --- a/go.mod +++ b/go.mod @@ -209,7 +209,7 @@ require ( cloud.google.com/go/compute/metadata v0.5.0 // indirect cloud.google.com/go/iam v1.2.0 // indirect github.com/Azure/azure-pipeline-go v0.2.3 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // @grafana/identity-access-team github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect diff --git a/pkg/login/social/connectors/azuread_oauth.go b/pkg/login/social/connectors/azuread_oauth.go index 484fb2fd3b8..c6b8691f4c7 100644 --- a/pkg/login/social/connectors/azuread_oauth.go +++ b/pkg/login/social/connectors/azuread_oauth.go @@ -10,6 +10,8 @@ import ( "strings" "time" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" jose "github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v3/jwt" "github.com/google/uuid" @@ -37,6 +39,14 @@ var ( errAzureADMissingGroups = &SocialError{"either the user does not have any group membership or the groups claim is missing from the token."} ) +// List of supported audiences in Azure +var supportedFederatedCredentialAudiences = []string{ + "api://AzureADTokenExchange", // Public + "api://AzureADTokenExchangeUSGov", // US Gov + "api://AzureADTokenExchangeChina", // Mooncake + "api://AzureADTokenExchangeUSNat", // USNat + "api://AzureADTokenExchangeUSSec"} // USSec + var _ social.SocialConnector = (*SocialAzureAD)(nil) var _ ssosettings.Reloadable = (*SocialAzureAD)(nil) @@ -168,6 +178,64 @@ func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token return userInfo, nil } +func (s *SocialAzureAD) Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + + switch s.info.ClientAuthentication { + case social.ManagedIdentity: + // Generate client assertion + clientAssertion, err := s.managedIdentityCallback(ctx) + if err != nil { + return nil, err + } + + // Set client assertion parameters + authOptions = append(authOptions, + oauth2.SetAuthURLParam("client_assertion", clientAssertion), + oauth2.SetAuthURLParam("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), + ) + + case social.ClientSecretPost: + // Default behavior for ClientSecretPost, no additional setup needed + + default: + return nil, fmt.Errorf("invalid client authentication method: %s", s.info.ClientAuthentication) + } + + // Default token exchange + return s.Config.Exchange(ctx, code, authOptions...) +} + +// ManagedIdentityCallback retrieves a token using the managed identity credential of the Azure service. +func (s *SocialAzureAD) managedIdentityCallback(ctx context.Context) (string, error) { + // Validate required fields for Managed Identity authentication + if s.info.ManagedIdentityClientID == "" { + return "", fmt.Errorf("ManagedIdentityClientID is required for Managed Identity authentication") + } + if s.info.FederatedCredentialAudience == "" { + return "", fmt.Errorf("FederatedCredentialAudience is required for Managed Identity authentication") + } + + // Prepare Managed Identity Credential + mic, err := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ + ID: azidentity.ClientID(s.info.ManagedIdentityClientID), + }) + if err != nil { + return "", fmt.Errorf("error constructing managed identity credential: %w", err) + } + + // Request token and return + tk, err := mic.GetToken(ctx, policy.TokenRequestOptions{ + Scopes: []string{fmt.Sprintf("%s/.default", s.info.FederatedCredentialAudience)}, + }) + if err != nil { + return "", fmt.Errorf("error getting managed identity token: %w", err) + } + + return tk.Token, nil +} + func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) if err != nil { @@ -206,6 +274,8 @@ func (s *SocialAzureAD) Validate(ctx context.Context, newSettings ssoModels.SSOS } return validation.Validate(info, requester, + validateClientAuthentication, + validateFederatedCredentialAudience, validateAllowedGroups, validation.MustBeEmptyValidator(info.ApiUrl, "API URL"), validation.RequiredUrlValidator(info.AuthUrl, "Auth URL"), @@ -281,6 +351,40 @@ func (s *SocialAzureAD) validateIDTokenSignature(ctx context.Context, client *ht return nil, &SocialError{"AzureAD OAuth: signing key not found"} } +func validateFederatedCredentialAudience(info *social.OAuthInfo, requester identity.Requester) error { + if info.ClientAuthentication != social.ManagedIdentity { + return nil + } + for _, supportedFederatedCredentialAudience := range supportedFederatedCredentialAudiences { + if info.FederatedCredentialAudience == supportedFederatedCredentialAudience { + return nil + } + } + return ssosettings.ErrInvalidOAuthConfig("FIC audience is not a supported audience.") +} + +func validateClientAuthentication(info *social.OAuthInfo, requester identity.Requester) error { + switch info.ClientAuthentication { + case social.ManagedIdentity: + if info.ManagedIdentityClientID == "" { + return ssosettings.ErrInvalidOAuthConfig("FIC managed identity client Id is required for Managed identity authentication.") + } + if info.FederatedCredentialAudience == "" { + return ssosettings.ErrInvalidOAuthConfig("FIC audience is required for Managed identity authentication.") + } + return nil + + case social.ClientSecretPost: + if info.ClientSecret == "" { + return ssosettings.ErrInvalidOAuthConfig("Client secret is required for Client secret authentication.") + } + return nil + + default: + return ssosettings.ErrInvalidOAuthConfig("Invalid client authentication method.") + } +} + func (claims *azureClaims) extractEmail() string { if claims.Email == "" { if claims.PreferredUsername != "" { diff --git a/pkg/login/social/connectors/azuread_oauth_test.go b/pkg/login/social/connectors/azuread_oauth_test.go index 8a25f9491a2..078f4b27802 100644 --- a/pkg/login/social/connectors/azuread_oauth_test.go +++ b/pkg/login/social/connectors/azuread_oauth_test.go @@ -1138,7 +1138,9 @@ func TestSocialAzureAD_Validate(t *testing.T) { name: "SSOSettings is valid", 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", @@ -1147,6 +1149,22 @@ func TestSocialAzureAD_Validate(t *testing.T) { }, requester: &user.SignedInUser{IsGrafanaAdmin: true}, }, + { + name: "SSOSettings is valid", + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_authentication": "managed_identity", + "client_id": "client-id", + "managed_identity_client_id": "managed-identity-client-id", + "federated_credential_audience": "api://AzureADTokenExchange", + "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", + }, + }, + requester: &user.SignedInUser{IsGrafanaAdmin: true}, + }, { name: "fails if settings map contains an invalid field", settings: ssoModels.SSOSettings{ diff --git a/pkg/login/social/connectors/common.go b/pkg/login/social/connectors/common.go index 7e9d62a6c76..255b96742e8 100644 --- a/pkg/login/social/connectors/common.go +++ b/pkg/login/social/connectors/common.go @@ -118,9 +118,19 @@ func createOAuthConfig(info *social.OAuthInfo, cfg *setting.Cfg, defaultName str authStyle = oauth2.AuthStyleAutoDetect } + var clientSecret string + switch info.ClientAuthentication { + case "client_secret_post": + clientSecret = info.ClientSecret + case "managed_identity": + clientSecret = "" + default: + clientSecret = info.ClientSecret + } + config := oauth2.Config{ ClientID: info.ClientId, - ClientSecret: info.ClientSecret, + ClientSecret: clientSecret, Endpoint: oauth2.Endpoint{ AuthURL: info.AuthUrl, TokenURL: info.TokenUrl, diff --git a/pkg/login/social/connectors/social_base.go b/pkg/login/social/connectors/social_base.go index 24e76d0c5fd..2de427299fe 100644 --- a/pkg/login/social/connectors/social_base.go +++ b/pkg/login/social/connectors/social_base.go @@ -119,8 +119,11 @@ func (s *SocialBase) getBaseSupportBundleContent(bf *bytes.Buffer) error { bf.WriteString(fmt.Sprintf("role_attribute_path = %v\n", s.info.RoleAttributePath)) bf.WriteString(fmt.Sprintf("role_attribute_strict = %v\n", s.info.RoleAttributeStrict)) bf.WriteString(fmt.Sprintf("skip_org_role_sync = %v\n", s.info.SkipOrgRoleSync)) + bf.WriteString(fmt.Sprintf("client_authentication = %v\n", s.info.ClientAuthentication)) bf.WriteString(fmt.Sprintf("client_id = %v\n", s.Config.ClientID)) bf.WriteString(fmt.Sprintf("client_secret = %v ; issue if empty\n", strings.Repeat("*", len(s.Config.ClientSecret)))) + bf.WriteString(fmt.Sprintf("managed_identity_client_id = %v\n", s.info.ManagedIdentityClientID)) + bf.WriteString(fmt.Sprintf("federated_credential_audience = %v\n", s.info.FederatedCredentialAudience)) bf.WriteString(fmt.Sprintf("auth_url = %v\n", s.Config.Endpoint.AuthURL)) bf.WriteString(fmt.Sprintf("token_url = %v\n", s.Config.Endpoint.TokenURL)) bf.WriteString(fmt.Sprintf("auth_style = %v\n", s.Config.Endpoint.AuthStyle)) diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index 12cf2dfd0a7..6eeaba06667 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -14,6 +14,12 @@ const ( OfflineAccessScope = "offline_access" RoleGrafanaAdmin = "GrafanaAdmin" // For AzureAD for example this value cannot contain spaces + // Values for ClientAuthentication under OAuthInfo (based on oidc spec) + ClientSecretPost = "client_secret_post" + // Azure AD + ManagedIdentity = "managed_identity" + // Other providers... + AzureADProviderName = "azuread" GenericOAuthProviderName = "generic_oauth" GitHubProviderName = "github" @@ -53,41 +59,44 @@ type SocialConnector interface { } type OAuthInfo struct { - AllowAssignGrafanaAdmin bool `mapstructure:"allow_assign_grafana_admin" toml:"allow_assign_grafana_admin"` - AllowSignup bool `mapstructure:"allow_sign_up" toml:"allow_sign_up"` - AllowedDomains []string `mapstructure:"allowed_domains" toml:"allowed_domains"` - AllowedGroups []string `mapstructure:"allowed_groups" toml:"allowed_groups"` - ApiUrl string `mapstructure:"api_url" toml:"api_url"` - AuthStyle string `mapstructure:"auth_style" toml:"auth_style"` - AuthUrl string `mapstructure:"auth_url" toml:"auth_url"` - AutoLogin bool `mapstructure:"auto_login" toml:"auto_login"` - ClientId string `mapstructure:"client_id" toml:"client_id"` - ClientSecret string `mapstructure:"client_secret" toml:"-"` - EmailAttributeName string `mapstructure:"email_attribute_name" toml:"email_attribute_name"` - EmailAttributePath string `mapstructure:"email_attribute_path" toml:"email_attribute_path"` - EmptyScopes bool `mapstructure:"empty_scopes" toml:"empty_scopes"` - Enabled bool `mapstructure:"enabled" toml:"enabled"` - GroupsAttributePath string `mapstructure:"groups_attribute_path" toml:"groups_attribute_path"` - HostedDomain string `mapstructure:"hosted_domain" toml:"hosted_domain"` - Icon string `mapstructure:"icon" toml:"icon"` - Name string `mapstructure:"name" toml:"name"` - RoleAttributePath string `mapstructure:"role_attribute_path" toml:"role_attribute_path"` - RoleAttributeStrict bool `mapstructure:"role_attribute_strict" toml:"role_attribute_strict"` - OrgAttributePath string `mapstructure:"org_attribute_path"` - OrgMapping []string `mapstructure:"org_mapping"` - Scopes []string `mapstructure:"scopes" toml:"scopes"` - SignoutRedirectUrl string `mapstructure:"signout_redirect_url" toml:"signout_redirect_url"` - SkipOrgRoleSync bool `mapstructure:"skip_org_role_sync" toml:"skip_org_role_sync"` - TeamIdsAttributePath string `mapstructure:"team_ids_attribute_path" toml:"team_ids_attribute_path"` - TeamsUrl string `mapstructure:"teams_url" toml:"teams_url"` - TlsClientCa string `mapstructure:"tls_client_ca" toml:"tls_client_ca"` - TlsClientCert string `mapstructure:"tls_client_cert" toml:"tls_client_cert"` - TlsClientKey string `mapstructure:"tls_client_key" toml:"tls_client_key"` - TlsSkipVerify bool `mapstructure:"tls_skip_verify_insecure" toml:"tls_skip_verify_insecure"` - 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"` - Extra map[string]string `mapstructure:",remain" toml:"extra,omitempty"` + AllowAssignGrafanaAdmin bool `mapstructure:"allow_assign_grafana_admin" toml:"allow_assign_grafana_admin"` + AllowSignup bool `mapstructure:"allow_sign_up" toml:"allow_sign_up"` + AllowedDomains []string `mapstructure:"allowed_domains" toml:"allowed_domains"` + AllowedGroups []string `mapstructure:"allowed_groups" toml:"allowed_groups"` + ApiUrl string `mapstructure:"api_url" toml:"api_url"` + AuthStyle string `mapstructure:"auth_style" toml:"auth_style"` + AuthUrl string `mapstructure:"auth_url" toml:"auth_url"` + AutoLogin bool `mapstructure:"auto_login" toml:"auto_login"` + ClientAuthentication string `mapstructure:"client_authentication" toml:"client_authentication"` + ClientId string `mapstructure:"client_id" toml:"client_id"` + ClientSecret string `mapstructure:"client_secret" toml:"-"` + ManagedIdentityClientID string `mapstructure:"managed_identity_client_id" toml:"managed_identity_client_id"` + FederatedCredentialAudience string `mapstructure:"federated_credential_audience" toml:"federated_credential_audience"` + EmailAttributeName string `mapstructure:"email_attribute_name" toml:"email_attribute_name"` + EmailAttributePath string `mapstructure:"email_attribute_path" toml:"email_attribute_path"` + EmptyScopes bool `mapstructure:"empty_scopes" toml:"empty_scopes"` + Enabled bool `mapstructure:"enabled" toml:"enabled"` + GroupsAttributePath string `mapstructure:"groups_attribute_path" toml:"groups_attribute_path"` + HostedDomain string `mapstructure:"hosted_domain" toml:"hosted_domain"` + Icon string `mapstructure:"icon" toml:"icon"` + Name string `mapstructure:"name" toml:"name"` + RoleAttributePath string `mapstructure:"role_attribute_path" toml:"role_attribute_path"` + RoleAttributeStrict bool `mapstructure:"role_attribute_strict" toml:"role_attribute_strict"` + OrgAttributePath string `mapstructure:"org_attribute_path"` + OrgMapping []string `mapstructure:"org_mapping"` + Scopes []string `mapstructure:"scopes" toml:"scopes"` + SignoutRedirectUrl string `mapstructure:"signout_redirect_url" toml:"signout_redirect_url"` + SkipOrgRoleSync bool `mapstructure:"skip_org_role_sync" toml:"skip_org_role_sync"` + TeamIdsAttributePath string `mapstructure:"team_ids_attribute_path" toml:"team_ids_attribute_path"` + TeamsUrl string `mapstructure:"teams_url" toml:"teams_url"` + TlsClientCa string `mapstructure:"tls_client_ca" toml:"tls_client_ca"` + TlsClientCert string `mapstructure:"tls_client_cert" toml:"tls_client_cert"` + TlsClientKey string `mapstructure:"tls_client_key" toml:"tls_client_key"` + TlsSkipVerify bool `mapstructure:"tls_skip_verify_insecure" toml:"tls_skip_verify_insecure"` + 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"` + Extra map[string]string `mapstructure:",remain" toml:"extra,omitempty"` } func NewOAuthInfo() *OAuthInfo { diff --git a/pkg/login/social/socialimpl/service_test.go b/pkg/login/social/socialimpl/service_test.go index 374fe6b6ba9..766fd501fea 100644 --- a/pkg/login/social/socialimpl/service_test.go +++ b/pkg/login/social/socialimpl/service_test.go @@ -218,8 +218,11 @@ icon = signin enabled = true allow_sign_up = false auto_login = true +client_authentication = test_client_authentication client_id = test_client_id client_secret = test_client_secret +managed_identity_client_id = test_managed_identity_client_id +federated_credential_audience = test_federated_credential_audience scopes = ["openid", "profile", "email"] empty_scopes = false email_attribute_name = email:primary @@ -257,38 +260,41 @@ signout_redirect_url = https://oauth.com/signout?post_logout_redirect_uri=https: require.NoError(t, err) expectedOAuthInfo := &social.OAuthInfo{ - Name: "OAuth", - Icon: "signin", - Enabled: true, - AllowSignup: false, - AutoLogin: true, - ClientId: "test_client_id", - ClientSecret: "test_client_secret", - Scopes: []string{"openid", "profile", "email"}, - EmptyScopes: false, - EmailAttributeName: "email:primary", - EmailAttributePath: "email", - RoleAttributePath: "role", - RoleAttributeStrict: true, - GroupsAttributePath: "groups", - TeamIdsAttributePath: "team_ids", - AuthUrl: "test_auth_url", - TokenUrl: "test_token_url", - ApiUrl: "test_api_url", - TeamsUrl: "test_teams_url", - AllowedDomains: []string{"domain1.com"}, - AllowedGroups: []string{}, - TlsSkipVerify: true, - TlsClientCert: "", - TlsClientKey: "", - TlsClientCa: "", - UsePKCE: false, - AuthStyle: "", - AllowAssignGrafanaAdmin: true, - UseRefreshToken: true, - SkipOrgRoleSync: true, - HostedDomain: "test_hosted_domain", - SignoutRedirectUrl: "https://oauth.com/signout?post_logout_redirect_uri=https://grafana.com", + Name: "OAuth", + Icon: "signin", + Enabled: true, + AllowSignup: false, + AutoLogin: true, + ClientAuthentication: "test_client_authentication", + ClientId: "test_client_id", + ClientSecret: "test_client_secret", + ManagedIdentityClientID: "test_managed_identity_client_id", + FederatedCredentialAudience: "test_federated_credential_audience", + Scopes: []string{"openid", "profile", "email"}, + EmptyScopes: false, + EmailAttributeName: "email:primary", + EmailAttributePath: "email", + RoleAttributePath: "role", + RoleAttributeStrict: true, + GroupsAttributePath: "groups", + TeamIdsAttributePath: "team_ids", + AuthUrl: "test_auth_url", + TokenUrl: "test_token_url", + ApiUrl: "test_api_url", + TeamsUrl: "test_teams_url", + AllowedDomains: []string{"domain1.com"}, + AllowedGroups: []string{}, + TlsSkipVerify: true, + TlsClientCert: "", + TlsClientKey: "", + TlsClientCa: "", + UsePKCE: false, + AuthStyle: "", + AllowAssignGrafanaAdmin: true, + UseRefreshToken: true, + SkipOrgRoleSync: true, + HostedDomain: "test_hosted_domain", + SignoutRedirectUrl: "https://oauth.com/signout?post_logout_redirect_uri=https://grafana.com", Extra: map[string]string{ "allowed_organizations": "org1, org2", "id_token_attribute_name": "id_token", diff --git a/pkg/services/ssosettings/strategies/oauth_strategy.go b/pkg/services/ssosettings/strategies/oauth_strategy.go index 2a9d70a1012..aa21dad6725 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy.go @@ -70,40 +70,43 @@ func (s *OAuthStrategy) loadSettingsForProvider(provider string) map[string]any section := s.cfg.Raw.Section("auth." + provider) result := map[string]any{ - "client_id": section.Key("client_id").Value(), - "client_secret": section.Key("client_secret").Value(), - "scopes": section.Key("scopes").Value(), - "empty_scopes": section.Key("empty_scopes").MustBool(false), - "auth_style": section.Key("auth_style").Value(), - "auth_url": section.Key("auth_url").Value(), - "token_url": section.Key("token_url").Value(), - "api_url": section.Key("api_url").Value(), - "teams_url": section.Key("teams_url").Value(), - "enabled": section.Key("enabled").MustBool(false), - "email_attribute_name": section.Key("email_attribute_name").Value(), - "email_attribute_path": section.Key("email_attribute_path").Value(), - "role_attribute_path": section.Key("role_attribute_path").Value(), - "role_attribute_strict": section.Key("role_attribute_strict").MustBool(false), - "groups_attribute_path": section.Key("groups_attribute_path").Value(), - "team_ids_attribute_path": section.Key("team_ids_attribute_path").Value(), - "allowed_domains": section.Key("allowed_domains").Value(), - "hosted_domain": section.Key("hosted_domain").Value(), - "allow_sign_up": section.Key("allow_sign_up").MustBool(false), - "name": section.Key("name").Value(), - "icon": section.Key("icon").Value(), - "skip_org_role_sync": section.Key("skip_org_role_sync").MustBool(false), - "tls_client_cert": section.Key("tls_client_cert").Value(), - "tls_client_key": section.Key("tls_client_key").Value(), - "tls_client_ca": section.Key("tls_client_ca").Value(), - "tls_skip_verify_insecure": section.Key("tls_skip_verify_insecure").MustBool(false), - "use_pkce": section.Key("use_pkce").MustBool(false), - "use_refresh_token": section.Key("use_refresh_token").MustBool(false), - "allow_assign_grafana_admin": section.Key("allow_assign_grafana_admin").MustBool(false), - "auto_login": section.Key("auto_login").MustBool(false), - "allowed_groups": section.Key("allowed_groups").Value(), - "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(), + "client_authentication": section.Key("client_authentication").Value(), + "client_id": section.Key("client_id").Value(), + "client_secret": section.Key("client_secret").Value(), + "managed_identity_client_id": section.Key("managed_identity_client_id").Value(), + "federated_credential_audience": section.Key("federated_credential_audience").Value(), + "scopes": section.Key("scopes").Value(), + "empty_scopes": section.Key("empty_scopes").MustBool(false), + "auth_style": section.Key("auth_style").Value(), + "auth_url": section.Key("auth_url").Value(), + "token_url": section.Key("token_url").Value(), + "api_url": section.Key("api_url").Value(), + "teams_url": section.Key("teams_url").Value(), + "enabled": section.Key("enabled").MustBool(false), + "email_attribute_name": section.Key("email_attribute_name").Value(), + "email_attribute_path": section.Key("email_attribute_path").Value(), + "role_attribute_path": section.Key("role_attribute_path").Value(), + "role_attribute_strict": section.Key("role_attribute_strict").MustBool(false), + "groups_attribute_path": section.Key("groups_attribute_path").Value(), + "team_ids_attribute_path": section.Key("team_ids_attribute_path").Value(), + "allowed_domains": section.Key("allowed_domains").Value(), + "hosted_domain": section.Key("hosted_domain").Value(), + "allow_sign_up": section.Key("allow_sign_up").MustBool(false), + "name": section.Key("name").Value(), + "icon": section.Key("icon").Value(), + "skip_org_role_sync": section.Key("skip_org_role_sync").MustBool(false), + "tls_client_cert": section.Key("tls_client_cert").Value(), + "tls_client_key": section.Key("tls_client_key").Value(), + "tls_client_ca": section.Key("tls_client_ca").Value(), + "tls_skip_verify_insecure": section.Key("tls_skip_verify_insecure").MustBool(false), + "use_pkce": section.Key("use_pkce").MustBool(false), + "use_refresh_token": section.Key("use_refresh_token").MustBool(false), + "allow_assign_grafana_admin": section.Key("allow_assign_grafana_admin").MustBool(false), + "auto_login": section.Key("auto_login").MustBool(false), + "allowed_groups": section.Key("allowed_groups").Value(), + "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(), } extraKeys := extraKeysByProvider[provider] diff --git a/pkg/services/ssosettings/strategies/oauth_strategy_test.go b/pkg/services/ssosettings/strategies/oauth_strategy_test.go index ba80610a49b..bc836879f9d 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy_test.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy_test.go @@ -19,8 +19,11 @@ var ( enabled = true allow_sign_up = false auto_login = true + client_authentication = test_client_authentication client_id = test_client_id client_secret = test_client_secret + managed_identity_client_id = test_managed_identity_client_id + federated_credential_audience = test_federated_credential_audience scopes = openid, profile, email empty_scopes = false email_attribute_name = email:primary @@ -57,45 +60,48 @@ var ( ` expectedOAuthInfo = map[string]any{ - "name": "OAuth", - "icon": "signin", - "enabled": true, - "allow_sign_up": false, - "auto_login": true, - "client_id": "test_client_id", - "client_secret": "test_client_secret", - "scopes": "openid, profile, email", - "empty_scopes": false, - "email_attribute_name": "email:primary", - "email_attribute_path": "email", - "role_attribute_path": "role", - "role_attribute_strict": true, - "groups_attribute_path": "groups", - "team_ids_attribute_path": "team_ids", - "auth_url": "test_auth_url", - "token_url": "test_token_url", - "api_url": "test_api_url", - "teams_url": "test_teams_url", - "allowed_domains": "domain1.com", - "allowed_groups": "", - "tls_skip_verify_insecure": true, - "tls_client_cert": "", - "tls_client_key": "", - "tls_client_ca": "", - "use_pkce": false, - "auth_style": "inheader", - "allow_assign_grafana_admin": true, - "use_refresh_token": true, - "hosted_domain": "test_hosted_domain", - "skip_org_role_sync": true, - "signout_redirect_url": "test_signout_redirect_url", - "allowed_organizations": "org1, org2", - "id_token_attribute_name": "id_token", - "login_attribute_path": "login", - "name_attribute_path": "name", - "team_ids": "first, second", - "org_attribute_path": "groups", - "org_mapping": "Group1:*:Editor", + "name": "OAuth", + "icon": "signin", + "enabled": true, + "allow_sign_up": false, + "auto_login": true, + "client_authentication": "test_client_authentication", + "client_id": "test_client_id", + "client_secret": "test_client_secret", + "managed_identity_client_id": "test_managed_identity_client_id", + "federated_credential_audience": "test_federated_credential_audience", + "scopes": "openid, profile, email", + "empty_scopes": false, + "email_attribute_name": "email:primary", + "email_attribute_path": "email", + "role_attribute_path": "role", + "role_attribute_strict": true, + "groups_attribute_path": "groups", + "team_ids_attribute_path": "team_ids", + "auth_url": "test_auth_url", + "token_url": "test_token_url", + "api_url": "test_api_url", + "teams_url": "test_teams_url", + "allowed_domains": "domain1.com", + "allowed_groups": "", + "tls_skip_verify_insecure": true, + "tls_client_cert": "", + "tls_client_key": "", + "tls_client_ca": "", + "use_pkce": false, + "auth_style": "inheader", + "allow_assign_grafana_admin": true, + "use_refresh_token": true, + "hosted_domain": "test_hosted_domain", + "skip_org_role_sync": true, + "signout_redirect_url": "test_signout_redirect_url", + "allowed_organizations": "org1, org2", + "id_token_attribute_name": "id_token", + "login_attribute_path": "login", + "name_attribute_path": "name", + "team_ids": "first, second", + "org_attribute_path": "groups", + "org_mapping": "Group1:*:Editor", } ) diff --git a/public/app/features/auth-config/fields.tsx b/public/app/features/auth-config/fields.tsx index 7214f79cfd2..d11d4eb9707 100644 --- a/public/app/features/auth-config/fields.tsx +++ b/public/app/features/auth-config/fields.tsx @@ -1,5 +1,6 @@ import { validate as uuidValidate } from 'uuid'; +import { SelectableValue } from '@grafana/data'; import { config } from '@grafana/runtime'; import { TextLink } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; @@ -26,8 +27,11 @@ export const sectionFields: Section = { id: 'general', fields: [ 'name', + 'clientAuthentication', 'clientId', 'clientSecret', + 'managedIdentityClientId', + 'federatedCredentialAudience', 'scopes', 'authUrl', 'tokenUrl', @@ -248,6 +252,18 @@ export const sectionFields: Section = { */ export function fieldMap(provider: string): Record { return { + clientAuthentication: { + label: 'Client authentication', + type: 'select', + description: 'The client authentication method used to authenticate to the token endpoint.', + multi: false, + options: clientAuthenticationOptions(provider), + defaultValue: { value: 'client_secret_post', label: 'Client secret' }, + validation: { + required: true, + message: 'This field is required', + }, + }, clientId: { label: 'Client Id', type: 'text', @@ -262,6 +278,16 @@ export function fieldMap(provider: string): Record { type: 'secret', description: 'The client secret of your OAuth2 app.', }, + managedIdentityClientId: { + label: 'FIC managed identity client Id', + type: 'text', + description: 'The managed identity client Id of the federated identity credential of your OAuth2 app.', + }, + federatedCredentialAudience: { + label: 'FIC audience', + type: 'text', + description: 'The audience of the federated identity credential of your OAuth2 app.', + }, allowedOrganizations: { label: 'Allowed organizations', type: 'select', @@ -652,3 +678,16 @@ function orgMappingDescription(provider: string): string { return 'List of "::" mappings.'; } } + +function clientAuthenticationOptions(provider: string): Array> { + switch (provider) { + case 'azuread': + return [ + { value: 'client_secret_post', label: 'Client secret' }, + { value: 'managed_identity', label: 'Managed identity' }, + ]; + // Other providers ... + default: + return [{ value: 'client_secret_post', label: 'Client secret' }]; + } +} diff --git a/public/app/features/auth-config/types.ts b/public/app/features/auth-config/types.ts index 7f15496ecd7..b831988aebd 100644 --- a/public/app/features/auth-config/types.ts +++ b/public/app/features/auth-config/types.ts @@ -21,8 +21,11 @@ export type SSOProviderSettingsBase = { authStyle?: string; authUrl?: string; autoLogin?: boolean; + clientAuthentication?: string; clientId: string; clientSecret: string; + managedIdentityClientId?: string; + federatedCredentialAudience?: string; emailAttributeName?: string; emailAttributePath?: string; emptyScopes?: boolean;