diff --git a/docs/operator-manual/user-management/index.md b/docs/operator-manual/user-management/index.md index 27426eb5cd..efcf68b020 100644 --- a/docs/operator-manual/user-management/index.md +++ b/docs/operator-manual/user-management/index.md @@ -599,3 +599,22 @@ Disabling certificate verification might make sense if: If either of those two applies, then you can disable OIDC provider certificate verification by setting `oidc.tls.insecure.skip.verify` to `"true"` in the `argocd-cm` ConfigMap. + +### Configurable groups claim + +By default, Argo CD reads user group information from the `groups` claim in the OIDC UserInfo endpoint response. + +Some identity providers return group membership using a different claim name such as `roles`, `memberof`, or other custom attributes. + +You can configure a custom claim name using the `groupsClaim` field: + +```yaml +oidc.config: | + name: example + issuer: https://example.com + clientID: example-client-id + clientSecret: example-secret + enableUserInfoGroups: true + userInfoPath: /userinfo + groupsClaim: roles +``` \ No newline at end of file diff --git a/util/oidc/oidc.go b/util/oidc/oidc.go index d856be7e04..900aaed531 100644 --- a/util/oidc/oidc.go +++ b/util/oidc/oidc.go @@ -908,7 +908,11 @@ func (a *ClientApp) SetGroupsFromUserInfo(ctx context.Context, claims jwt.Claims if groupClaims["sub"] != userInfo["sub"] { return groupClaims, errors.New("subject of claims from user info endpoint didn't match subject of idToken, see https://openid.net/specs/openid-connect-core-1_0.html#UserInfo") } - groupClaims["groups"] = userInfo["groups"] + groupsClaim := a.settings.UserInfoGroupsClaim() + if userInfo[groupsClaim] == nil { + log.Warnf("groups claim '%s' not found in UserInfo response, user will have no groups", groupsClaim) + } + groupClaims["groups"] = userInfo[groupsClaim] } return groupClaims, nil diff --git a/util/settings/settings.go b/util/settings/settings.go index 7894404362..1159233d9d 100644 --- a/util/settings/settings.go +++ b/util/settings/settings.go @@ -198,6 +198,7 @@ func (o *oidcConfig) toExported() *OIDCConfig { RootCA: o.RootCA, EnablePKCEAuthentication: o.EnablePKCEAuthentication, DomainHint: o.DomainHint, + GroupsClaim: o.GroupsClaim, } } @@ -218,6 +219,7 @@ type OIDCConfig struct { DomainHint string `json:"domainHint,omitempty"` Azure *AzureOIDCConfig `json:"azure,omitempty"` RefreshTokenThreshold string `json:"refreshTokenThreshold,omitempty"` + GroupsClaim string `json:"groupsClaim,omitempty"` } type AzureOIDCConfig struct { @@ -2502,6 +2504,14 @@ func (mgr *SettingsManager) GetAllowedNodeLabels() []string { return labelKeys } +func (a *ArgoCDSettings) UserInfoGroupsClaim() string { + cfg := a.OIDCConfig() + if cfg != nil && cfg.GroupsClaim != "" { + return strings.TrimSpace(cfg.GroupsClaim) + } + return "groups" +} + // IsInClusterEnabled returns false if in-cluster is explicitly disabled in argocd-cm configmap, true otherwise func (mgr *SettingsManager) IsInClusterEnabled() (bool, error) { argoCDCM, err := mgr.getConfigMap() diff --git a/util/settings/settings_test.go b/util/settings/settings_test.go index 8a3203b895..fe62a91921 100644 --- a/util/settings/settings_test.go +++ b/util/settings/settings_test.go @@ -2351,6 +2351,30 @@ func TestSettingsManager_GetAllowedNodeLabels(t *testing.T) { } } +func TestUserInfoGroupsClaim(t *testing.T) { + t.Run("should return default 'groups' when config is empty", func(t *testing.T) { + settings := &ArgoCDSettings{ + OIDCConfigRAW: "", + } + + result := settings.UserInfoGroupsClaim() + assert.Equal(t, "groups", result) + }) + + t.Run("should return default 'groups' when groupsClaim not present", func(t *testing.T) { + settings := &ArgoCDSettings{ + OIDCConfigRAW: `{ + "name": "test", + "issuer": "https://example.com", + "clientID": "test-client" + }`, + } + + result := settings.UserInfoGroupsClaim() + assert.Equal(t, "groups", result) + }) +} + func TestSecretsInformerExcludesClusterSecrets(t *testing.T) { cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{