fix: AAD Domain Hint Improvement (fixes #18066) (#24639)

Signed-off-by: pjmanda <jhansi.manda@amadeus.com>
Signed-off-by: jhansireddy01 <m.jhansireddy01@gmail.com>
Co-authored-by: pjmanda <jhansi.manda@amadeus.com>
Co-authored-by: Nitish Kumar <justnitish06@gmail.com>
Co-authored-by: jhansireddy01 <m.jhansireddy01@gmail.com>
This commit is contained in:
jhansi 2026-03-10 19:51:13 +05:30 committed by GitHub
parent 845d1c9c40
commit 054538b069
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 133 additions and 2 deletions

View file

@ -241,3 +241,33 @@ data:
Context 'my-argo-cd-url' updated
You may get an warning if you are not using a correctly signed certs. Refer to [Why Am I Getting x509: certificate signed by unknown authority When Using The CLI?](https://argo-cd.readthedocs.io/en/stable/faq/#why-am-i-getting-x509-certificate-signed-by-unknown-authority-when-using-the-cli).
## Domain hint (optional)
For Microsoft identity platforms, you can set `domainHint` in `oidc.config` to provide a domain hint during sign-in.
When configured, Argo CD adds `domain_hint=<value>` to the authorization request sent to Microsoft.
This can reduce account discovery prompts in multi-tenant or federated environments.
- **Field:** `domainHint`
- **Type:** `string`
- **Required:** No
- **Default:** empty (parameter is not sent)
Example:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
oidc.config: |
name: Microsoft
issuer: https://login.microsoftonline.com/<tenant-id>/v2.0
clientID: <client-id>
clientSecret: $oidc.microsoft.clientSecret
requestedScopes: ["openid", "profile", "email", "groups"]
domainHint: contoso.com
```

View file

@ -100,8 +100,10 @@ type ClientApp struct {
provider Provider
// clientCache represent a cache of sso artifact
clientCache cache.CacheClient
// properties for azure workload identity.
azure azureApp
// domainHint is an optional hint to the identity provider about the domain the user belongs to.
// Used to pre-fill or streamline the login experience (e.g., for Azure AD multi-tenant scenarios).
domainHint string
azure azureApp
// preemptive token refresh threshold
refreshTokenThreshold time.Duration
}
@ -182,6 +184,14 @@ func GetScopesOrDefault(scopes []string) []string {
return scopes
}
func getDomainHint(settings *settings.ArgoCDSettings) string {
oidcConfig := settings.OIDCConfig()
if oidcConfig != nil {
return strings.TrimSpace(oidcConfig.DomainHint)
}
return ""
}
// NewClientApp will register the Argo CD client app (either via Dex or external OIDC) and return an
// object which has HTTP handlers for handling the HTTP responses for login and callback
func NewClientApp(settings *settings.ArgoCDSettings, dexServerAddr string, dexTLSConfig *dex.DexTLSConfig, baseHRef string, cacheClient cache.CacheClient) (*ClientApp, error) {
@ -193,6 +203,7 @@ func NewClientApp(settings *settings.ArgoCDSettings, dexServerAddr string, dexTL
if err != nil {
return nil, err
}
domainHint := getDomainHint(settings)
a := ClientApp{
clientID: settings.OAuth2ClientID(),
clientSecret: settings.OAuth2ClientSecret(),
@ -204,6 +215,7 @@ func NewClientApp(settings *settings.ArgoCDSettings, dexServerAddr string, dexTL
encryptionKey: encryptionKey,
clientCache: cacheClient,
azure: azureApp{mtx: &sync.RWMutex{}},
domainHint: domainHint,
refreshTokenThreshold: settings.OIDCRefreshTokenThreshold,
}
log.Infof("Creating client app (%s)", a.clientID)
@ -422,6 +434,9 @@ func (a *ClientApp) HandleLogin(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Invalid redirect URL: the protocol and host (including port) must match and the path must be within allowed URLs if provided", http.StatusBadRequest)
return
}
if a.domainHint != "" {
opts = append(opts, oauth2.SetAuthURLParam("domain_hint", a.domainHint))
}
if a.usePKCE {
pkceVerifier = oauth2.GenerateVerifier()
opts = append(opts, oauth2.S256ChallengeOption(pkceVerifier))

View file

@ -49,6 +49,65 @@ func setupAzureIdentity(t *testing.T) {
t.Setenv("AZURE_FEDERATED_TOKEN_FILE", tokenFilePath)
}
func TestGetDomainHint(t *testing.T) {
t.Run("Returns domain hint when OIDC config is set", func(t *testing.T) {
settings := &settings.ArgoCDSettings{
OIDCConfigRAW: `
name: Test OIDC
issuer: https://example.com
clientID: test-client
clientSecret: test-secret
domainHint: example.com
`,
}
domainHint := getDomainHint(settings)
assert.Equal(t, "example.com", domainHint)
})
t.Run("Returns empty string when domain hint is not set", func(t *testing.T) {
settings := &settings.ArgoCDSettings{
OIDCConfigRAW: `
name: Test OIDC
issuer: https://example.com
clientID: test-client
clientSecret: test-secret
`,
}
domainHint := getDomainHint(settings)
assert.Empty(t, domainHint)
})
t.Run("Returns empty string when OIDC config is nil", func(t *testing.T) {
settings := &settings.ArgoCDSettings{
OIDCConfigRAW: "",
}
domainHint := getDomainHint(settings)
assert.Empty(t, domainHint)
})
t.Run("Returns empty string when YAML is malformed", func(t *testing.T) {
settings := &settings.ArgoCDSettings{
OIDCConfigRAW: `{this is not valid yaml at all]`,
}
domainHint := getDomainHint(settings)
assert.Empty(t, domainHint)
})
t.Run("Trims whitespaces from domain hint", func(t *testing.T) {
settings := &settings.ArgoCDSettings{
OIDCConfigRAW: `
name: Test OIDC
issuer: https://example.com
clientID: test-client
clientSecret: test-secret
domainHint: " example.com "
`,
}
domainHint := getDomainHint(settings)
assert.Equal(t, "example.com", domainHint)
})
}
func TestInferGrantType(t *testing.T) {
for _, path := range []string{"dex", "okta", "auth0", "onelogin"} {
t.Run(path, func(t *testing.T) {
@ -102,6 +161,33 @@ func TestIDTokenClaims(t *testing.T) {
assert.JSONEq(t, "{\"id_token\":{\"groups\":{\"essential\":true}}}", values.Get("claims"))
}
func TestHandleLogin_IncludesDomainHint(t *testing.T) {
oidcTestServer := test.GetOIDCTestServer(t, nil)
t.Cleanup(oidcTestServer.Close)
cdSettings := &settings.ArgoCDSettings{
URL: "https://argocd.example.com",
OIDCTLSInsecureSkipVerify: true,
OIDCConfigRAW: fmt.Sprintf(`
name: Test
issuer: %s
clientID: test-client-id
clientSecret: test-client-secret
domainHint: example.com
requestedScopes: ["openid", "profile", "email", "groups"]`, oidcTestServer.URL),
}
app, err := NewClientApp(cdSettings, "", nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
require.NoError(t, err)
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "https://argocd.example.com/auth/login", http.NoBody)
w := httptest.NewRecorder()
app.HandleLogin(w, req)
assert.Equal(t, http.StatusSeeOther, w.Code)
location := w.Header().Get("Location")
assert.Contains(t, location, "domain_hint=example.com")
}
type fakeProvider struct {
EndpointError bool
}