Copilot Usage Confirmation
MSAL Version
2.14.1
Description
Description
acquireTokenSilent never succeeds for a guest account in a CIAM (External ID) tenant — i.e. a user whose home tenant is a different Entra tenant. Interactive sign-in works fine and returns a valid token; only silent renewal fails. Because the access token lives ~65 minutes, affected users are forced to sign in again roughly every hour.
Environment: MSAL for iOS 2.14.1 (SPM binary target), iOS 26.4, real device. Authority is MSALCIAMAuthority with a vanity domain: https://.ciamlogin.com/.
The cached account is the problem: its only tenant profile carries the CIAM tenant's object id but is labelled with the home tenant id. MSAL then derives the refresh authority from that home tenant while keeping the CIAM host, and the server rejects the pairing.
Workarounds attempted, none of which work:
Setting MSALSilentTokenParameters.authority to the CIAM authority — MSAL substitutes the account's home tenant regardless.
accountForIdentifier: with . (using either the home or CIAM object id) — no match.
accountsForParameters: with tenantProfileIdentifier set to the CIAM object id — returns the same home-tenant-labelled account.
There is no cached record for the CIAM tenant to target, and no public API to override the realm.
Cross-platform note: MSAL-Android has the same underlying defect (AzureAD/microsoft-authentication-library-for-android#2172, #2073). It surfaces earlier there — the local cache lookup misses and fails before any network call — and can be worked around by wrapping the account so getTenantId() returns the realm while getHomeAccountId() stays real. That is not portable to iOS: MSALAccount has no separate realm field, so correcting the tenant also changes the key used to locate the refresh token.
Error Details
domain = MSALErrorDomain
code = -50002 (MSALErrorInteractionRequired)
oauthError = server_error
description = "User interaction is required"
underlying = MSALErrorDomain / -50000 (MSALErrorInternal)
Two things worth noting:
oauthError is server_error, not invalid_grant — the refresh token itself is valid; the request is malformed.
MSALSTSErrorCodesKey is not populated, so the actual AADSTS code is unavailable from the NSError and can only be found in MSAL's verbose log.
MSAL Logs
Server's actual reason:
AADSTS500210: Domain name does not match with the tenant identifier
MSAL finds the refresh token, then targets the home tenant while keeping the CIAM host:
[MSAL] Found app refresh token.
[MSAL] Trying to acquire access token using App Refresh Token for
authority https://.ciamlogin.com/
account .
The cached account, showing the mislabelled realm:
homeAccountId = .
environment = .ciamlogin.com
tenantProfiles: 1
tenantId = id = isHomeTenantProfile = true
is exactly the oid claim of the issued access token, whose tid is the CIAM tenant — so the realm recorded for that profile is wrong.
Reproduction Steps
Reproduction Steps
Configure MSALPublicClientApplication with an MSALCIAMAuthority pointing at a CIAM tenant (vanity domain).
Sign in interactively with an Entra account whose home tenant is a different tenant — a guest in the CIAM tenant.
Confirm sign-in succeeds. The issued access token has tid = CIAM tenant, oid = the user's object id in the CIAM tenant.
Call acquireTokenSilent with that account. Set MSALSilentTokenParameters.forceRefresh = true to reproduce immediately instead of waiting ~65 minutes for the access token to expire.
The call fails as above. Repeating it always fails; only a fresh interactive sign-in recovers.
A local (non-guest) CIAM account on the same tenant and app registration renews correctly, which isolates this to the guest/federated case.
Expected Behavior
Expected behaviour
acquireTokenSilent should renew the token using the authority that issued it.
Either of the following would resolve it:
For CIAM, resolve the refresh authority from the request authority (or id_token.tid) rather than client_info.utid when the two differ — as suggested in #2871.
Expose the refresh token for the browser-delegated CIAM flow the same way it already is for native auth (MSALNativeAuthTokenResult.refreshToken / returnRefreshToken, added in 2.9.0), so apps can redeem it against the correct authority themselves.
Regression
No response
Screenshots & Screen Recordings
No response
Additional context
No response
Copilot Usage Confirmation
MSAL Version
2.14.1
Description
Description
acquireTokenSilent never succeeds for a guest account in a CIAM (External ID) tenant — i.e. a user whose home tenant is a different Entra tenant. Interactive sign-in works fine and returns a valid token; only silent renewal fails. Because the access token lives ~65 minutes, affected users are forced to sign in again roughly every hour.
Environment: MSAL for iOS 2.14.1 (SPM binary target), iOS 26.4, real device. Authority is MSALCIAMAuthority with a vanity domain: https://.ciamlogin.com/.
The cached account is the problem: its only tenant profile carries the CIAM tenant's object id but is labelled with the home tenant id. MSAL then derives the refresh authority from that home tenant while keeping the CIAM host, and the server rejects the pairing.
Workarounds attempted, none of which work:
Setting MSALSilentTokenParameters.authority to the CIAM authority — MSAL substitutes the account's home tenant regardless.
accountForIdentifier: with . (using either the home or CIAM object id) — no match.
accountsForParameters: with tenantProfileIdentifier set to the CIAM object id — returns the same home-tenant-labelled account.
There is no cached record for the CIAM tenant to target, and no public API to override the realm.
Cross-platform note: MSAL-Android has the same underlying defect (AzureAD/microsoft-authentication-library-for-android#2172, #2073). It surfaces earlier there — the local cache lookup misses and fails before any network call — and can be worked around by wrapping the account so getTenantId() returns the realm while getHomeAccountId() stays real. That is not portable to iOS: MSALAccount has no separate realm field, so correcting the tenant also changes the key used to locate the refresh token.
Error Details
domain = MSALErrorDomain
code = -50002 (MSALErrorInteractionRequired)
oauthError = server_error
description = "User interaction is required"
underlying = MSALErrorDomain / -50000 (MSALErrorInternal)
Two things worth noting:
oauthError is server_error, not invalid_grant — the refresh token itself is valid; the request is malformed.
MSALSTSErrorCodesKey is not populated, so the actual AADSTS code is unavailable from the NSError and can only be found in MSAL's verbose log.
MSAL Logs
Server's actual reason:
AADSTS500210: Domain name does not match with the tenant identifier
MSAL finds the refresh token, then targets the home tenant while keeping the CIAM host:
[MSAL] Found app refresh token.
[MSAL] Trying to acquire access token using App Refresh Token for
authority https://.ciamlogin.com/
account .
The cached account, showing the mislabelled realm:
homeAccountId = .
environment = .ciamlogin.com
tenantProfiles: 1
tenantId = id = isHomeTenantProfile = true
is exactly the oid claim of the issued access token, whose tid is the CIAM tenant — so the realm recorded for that profile is wrong.
Reproduction Steps
Reproduction Steps
Configure MSALPublicClientApplication with an MSALCIAMAuthority pointing at a CIAM tenant (vanity domain).
Sign in interactively with an Entra account whose home tenant is a different tenant — a guest in the CIAM tenant.
Confirm sign-in succeeds. The issued access token has tid = CIAM tenant, oid = the user's object id in the CIAM tenant.
Call acquireTokenSilent with that account. Set MSALSilentTokenParameters.forceRefresh = true to reproduce immediately instead of waiting ~65 minutes for the access token to expire.
The call fails as above. Repeating it always fails; only a fresh interactive sign-in recovers.
A local (non-guest) CIAM account on the same tenant and app registration renews correctly, which isolates this to the guest/federated case.
Expected Behavior
Expected behaviour
acquireTokenSilent should renew the token using the authority that issued it.
Either of the following would resolve it:
For CIAM, resolve the refresh authority from the request authority (or id_token.tid) rather than client_info.utid when the two differ — as suggested in #2871.
Expose the refresh token for the browser-delegated CIAM flow the same way it already is for native auth (MSALNativeAuthTokenResult.refreshToken / returnRefreshToken, added in 2.9.0), so apps can redeem it against the correct authority themselves.
Regression
No response
Screenshots & Screen Recordings
No response
Additional context
No response