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 <jack.baldry@grafana.com> * 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 <jack.baldry@grafana.com> Co-authored-by: Mihaly Gyongyosi <mgyongyosi@users.noreply.github.com> Co-authored-by: Jack Baldry <jack.baldry@grafana.com>
This commit is contained in:
co-authored by
Mihaly Gyongyosi
Jack Baldry
parent
d96f378562
commit
79d565f285
@@ -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/<tenant-id>/oauth2/v2.0/authorize
|
||||
token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
|
||||
|
||||
@@ -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/<tenant-id>/oauth2/v2.0/authorize
|
||||
;token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
|
||||
|
||||
+58
-21
@@ -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" %}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
+44
-35
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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<string, FieldData> {
|
||||
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<string, FieldData> {
|
||||
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 "<ExternalName>:<OrgIdOrName>:<Role>" mappings.';
|
||||
}
|
||||
}
|
||||
|
||||
function clientAuthenticationOptions(provider: string): Array<SelectableValue<string>> {
|
||||
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' }];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user