Skip to content

Redeem authorization code with MSAL when complex client credentials are configured (#3631)#3925

Open
armin-azar wants to merge 2 commits into
AzureAD:masterfrom
armin-azar:fix/3631-redeem-authcode-when-complexclient
Open

Redeem authorization code with MSAL when complex client credentials are configured (#3631)#3925
armin-azar wants to merge 2 commits into
AzureAD:masterfrom
armin-azar:fix/3631-redeem-authcode-when-complexclient

Conversation

@armin-azar

Copy link
Copy Markdown
Contributor

Redeem authorization code with MSAL when complex client credentials are configured (#3631)

  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

Summary of the changes (Less than 80 chars)
Redeem auth code with MSAL for complex credentials in sign-in-only apps

Description

Fixes #3631

See Known limitations below.
Opening as a interim so the maintainers can weigh in on the design trade-offs before this is finalized.

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 with ResponseType=code, sign-in fails with AADSTS7000218 (client_assertion or client_secret required).

Root cause: the default ASP.NET Core OpenIdConnectHandler performs the authorization-code redemption on the callback. It has no knowledge of the complex credential, so it sends the token request without a client_assertion, and Azure AD rejects it. Microsoft.Identity.Web only hooks OnAuthorizationCodeReceived (and therefore redeems the code via MSAL with the correct credential) when EnableTokenAcquisitionToCallDownstreamApi() 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:

  • Web app registered in Entra ID, configured with a federated credential (SignedAssertionFromManagedIdentity) and ResponseType=code, sign-in only (no EnableTokenAcquisitionToCallDownstreamApi()).
  • Sign-in consistently failed on the callback with AADSTS7000218, confirming the report.

Fix

Microsoft.Identity.Web now detects the scenario and redeems the code with MSAL automatically:

  • Detection: a credential is considered "complex" when any ClientCredentials entry has a SourceType other than ClientSecret; combined with ResponseType=code this triggers the automatic path.
  • Wiring: OnAuthorizationCodeReceived is wired via a PostConfigure<OpenIdConnectOptions> step so it runs after all Configure steps, including the one EnableTokenAcquisitionToCallDownstreamApi() registers.
  • Services: the token acquisition services and a minimal internal no-op IMsalTokenCacheProvider are 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.
  • Diagnostics: instead of a bare AADSTS7000218, the library logs an informational message when auto-redemption is enabled, and a warning when it is needed but the token acquisition services are not available.

Backward compatibility

  • EnableTokenAcquisitionToCallDownstreamApi() is unchanged. A flag on MergedOptions (AuthorizationCodeHandledByMicrosoftIdentityWeb) records that the code is already being redeemed by Microsoft.Identity.Web, so the automatic path never wires a second, redundant handler.
  • Plain client secret with ResponseType=code is unaffected. The OIDC handler already redeems those correctly, and the automatic path does not activate.
  • Existing workaround keeps working. Apps that adopted the workaround from the issue thread, calling EnableTokenAcquisitionToCallDownstreamApi() and registering a dummy or no-op IMsalTokenCacheProvider, 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.
  • Apps that already redeem the code themselves in a custom OnAuthorizationCodeReceived handler 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

  • Repro before the change: confirmed AADSTS7000218 against a real Entra ID tenant (see Reproduction above).
  • Smoke test after the change: with the same Entra ID app registration (federated credential plus ResponseType=code, sign-in only, no EnableTokenAcquisitionToCallDownstreamApi()), the interactive sign-in path now completes successfully; the code is redeemed with MSAL using the configured credential.
  • Unit tests added (WebAppExtensionsTests):
    • Automatic redemption fires for SignedAssertionFromManagedIdentity plus ResponseType=code without EnableTokenAcquisitionToCallDownstreamApi(), and the token acquisition and cache services are registered automatically.
    • No double redemption: when the app already redeems the code in its own handler, MSAL redemption is skipped.
    • Plain client secret plus ResponseType=code does not auto-enable token acquisition (no added service footprint).
  • Full existing unit test suite passes (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:

  1. Token cache slot. The fallback no-op IMsalTokenCacheProvider is registered eagerly with TryAddSingleton. If an app sets ResponseType=code directly on its own options alongside a complex credential and later calls .AddInMemoryTokenCaches() or .AddDistributedTokenCaches() (also TryAddSingleton, first registration wins), the real cache is shadowed by the no-op. Options: register the real MsalMemoryTokenCacheProvider instead, defer cache registration, or let TokenAcquisition tolerate a missing provider.
  2. Sign-out. The automatic path wires OnRedirectToIdentityProviderForSignOut to call RemoveAccountAsync, 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.
  3. B2C. The automatic path does not perform the client_info (uid/utid) claim injection that the EnableTokenAcquisitionToCallDownstreamApi() path does in OnTokenValidated, so account cleanup at sign-out may not locate the account in B2C or CIAM scenarios.
  4. Eager probe. To decide whether to register services at ConfigureServices time, the options-configuration delegate is invoked once against a throwaway options instance, wrapped in a broad catch. This runs for every AddMicrosoftIdentityWebApp call, which means a delegate with side effects runs an extra time; it also cannot observe a ResponseType set directly on OpenIdConnectOptions (only what the delegate sets), so that sub-case is detected at PostConfigure time 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Authorization code should always be redeemed with MSAL

1 participant