Redeem authorization code with MSAL when complex client credentials are configured (#3631)#3925
Open
armin-azar wants to merge 2 commits into
Open
Conversation
…lient credentials are configured
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Redeem authorization code with MSAL when complex client credentials are configured (#3631)
Summary of the changes (Less than 80 chars)
Redeem auth code with MSAL for complex credentials in sign-in-only apps
Description
Fixes #3631
Problem
When a web app is configured with a complex client credential, meaning
SignedAssertionFromManagedIdentity, a certificate, or anything other than a plain client secret, together withResponseType=code, sign-in fails with AADSTS7000218 (client_assertionorclient_secretrequired).Root cause: the default ASP.NET Core
OpenIdConnectHandlerperforms the authorization-code redemption on the callback. It has no knowledge of the complex credential, so it sends the token request without aclient_assertion, and Azure AD rejects it. Microsoft.Identity.Web only hooksOnAuthorizationCodeReceived(and therefore redeems the code via MSAL with the correct credential) whenEnableTokenAcquisitionToCallDownstreamApi()is called. A sign-in-only app has no reason to call that method, so it hits the failure with no actionable error explaining why.Reproduction
I reproduced this end-to-end against my own Azure test subscription and Entra ID tenant before making any changes:
SignedAssertionFromManagedIdentity) andResponseType=code, sign-in only (noEnableTokenAcquisitionToCallDownstreamApi()).Fix
Microsoft.Identity.Web now detects the scenario and redeems the code with MSAL automatically:
ClientCredentialsentry has aSourceTypeother thanClientSecret; combined withResponseType=codethis triggers the automatic path.OnAuthorizationCodeReceivedis wired via aPostConfigure<OpenIdConnectOptions>step so it runs after allConfiguresteps, including the oneEnableTokenAcquisitionToCallDownstreamApi()registers.IMsalTokenCacheProviderare registered so a sign-in-only app does not have to register a token cache. The no-op type is kept internal and is not part of the public API.Backward compatibility
EnableTokenAcquisitionToCallDownstreamApi()is unchanged. A flag onMergedOptions(AuthorizationCodeHandledByMicrosoftIdentityWeb) records that the code is already being redeemed by Microsoft.Identity.Web, so the automatic path never wires a second, redundant handler.ResponseType=codeis unaffected. The OIDC handler already redeems those correctly, and the automatic path does not activate.EnableTokenAcquisitionToCallDownstreamApi()and registering a dummy or no-opIMsalTokenCacheProvider, will not break after upgrading: that path still sets the flag above (so the automatic path stays off), and the service registrations are idempotent, so the dummy cache continues to be used exactly as before.OnAuthorizationCodeReceivedhandler are not double-redeemed: the automatic handler invokes the application handler first and only redeems with MSAL if the code has not already been handled (AuthorizationCodeReceivedContext.HandledCodeRedemption), which avoids replaying the single-use code.Testing
ResponseType=code, sign-in only, noEnableTokenAcquisitionToCallDownstreamApi()), the interactive sign-in path now completes successfully; the code is redeemed with MSAL using the configured credential.WebAppExtensionsTests):SignedAssertionFromManagedIdentityplusResponseType=codewithoutEnableTokenAcquisitionToCallDownstreamApi(), and the token acquisition and cache services are registered automatically.ResponseType=codedoes not auto-enable token acquisition (no added service footprint).net8.0): 991 passed, 11 pre-existing skips.Known limitations and open questions for maintainers
The automatic activation is convenient but has unconditional side effects, and I would like the team's guidance on how far to take this before it merges:
IMsalTokenCacheProvideris registered eagerly withTryAddSingleton. If an app setsResponseType=codedirectly on its own options alongside a complex credential and later calls.AddInMemoryTokenCaches()or.AddDistributedTokenCaches()(alsoTryAddSingleton, first registration wins), the real cache is shadowed by the no-op. Options: register the realMsalMemoryTokenCacheProviderinstead, defer cache registration, or letTokenAcquisitiontolerate a missing provider.OnRedirectToIdentityProviderForSignOutto callRemoveAccountAsync, which builds the confidential client (loading the credential). A credential outage could turn a previously pure-redirect sign-out into a failure for a sign-in-only app.client_info(uid/utid) claim injection that theEnableTokenAcquisitionToCallDownstreamApi()path does inOnTokenValidated, so account cleanup at sign-out may not locate the account in B2C or CIAM scenarios.ConfigureServicestime, the options-configuration delegate is invoked once against a throwaway options instance, wrapped in a broadcatch. This runs for everyAddMicrosoftIdentityWebAppcall, which means a delegate with side effects runs an extra time; it also cannot observe aResponseTypeset directly onOpenIdConnectOptions(only what the delegate sets), so that sub-case is detected atPostConfiguretime and logs a warning rather than redeeming. A design that always registers the (idempotent, cheap) token acquisition services and drops the probe would remove this, at the cost of resolving the cache-slot question in item 1.Happy to iterate on any of these based on the direction you prefer.
Fixes #3631.