Refactor client credential material resolution#5835
Conversation
There was a problem hiding this comment.
Pull request overview
Refactors confidential client credential handling by introducing a normalized “credential material” model (context in → material out) and updating the token request pipeline to resolve/apply that material consistently, including mTLS/PoP constraints and diagnostics.
Changes:
- Introduces
CredentialContext,CredentialMaterial,CredentialMaterialResolver,CredentialSource, andClientAuthMode; updatesIClientCredentialto returnCredentialMaterial. - Updates
TokenClientto resolve the token endpoint once and pass it through credential material resolution, then apply returned body parameters and resolved certificate. - Adds
MsalError.InvalidCredentialMaterialand adds unit coverage viaCredentialMatrixTests.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Microsoft.Identity.Test.Unit/RequestsTests/CredentialMatrixTests.cs | Adds matrix/edge-case tests for credential material resolution across Regular vs mTLS modes. |
| src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt | Public API addition for MsalError.InvalidCredentialMaterial. |
| src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt | Public API addition for MsalError.InvalidCredentialMaterial. |
| src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt | Public API addition for MsalError.InvalidCredentialMaterial. |
| src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt | Public API addition for MsalError.InvalidCredentialMaterial. |
| src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt | Public API addition for MsalError.InvalidCredentialMaterial. |
| src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt | Public API addition for MsalError.InvalidCredentialMaterial. |
| src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs | Uses CredentialMaterialResolver and applies returned request parameters + resolved certificate. |
| src/client/Microsoft.Identity.Client/MsalError.cs | Adds InvalidCredentialMaterial error code with guidance. |
| src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj | Adds item entries for new credential files (currently introduces duplicate compile item risk). |
| src/client/Microsoft.Identity.Client/Internal/ClientCredential/IClientCredential.cs | Replaces “apply to OAuth2Client” API with GetCredentialMaterialAsync(context, ct). |
| src/client/Microsoft.Identity.Client/Internal/ClientCredential/CredentialSource.cs | New enum indicating static vs callback origin. |
| src/client/Microsoft.Identity.Client/Internal/ClientCredential/CredentialMaterialResolver.cs | New centralized resolver building context + validating returned material. |
| src/client/Microsoft.Identity.Client/Internal/ClientCredential/CredentialMaterial.cs | New normalized output type for credential resolution. |
| src/client/Microsoft.Identity.Client/Internal/ClientCredential/CredentialContext.cs | New immutable input context type for credential resolution. |
| src/client/Microsoft.Identity.Client/Internal/ClientCredential/ClientCredentialApplicationResult.cs | Removes legacy result type. |
| src/client/Microsoft.Identity.Client/Internal/ClientCredential/ClientAuthMode.cs | New enum for Regular vs MtlsMode. |
| src/client/Microsoft.Identity.Client/Internal/ClientCredential/ClientAssertionStringDelegateCredential.cs | Updates to return CredentialMaterial and enforce mTLS incompatibility. |
| src/client/Microsoft.Identity.Client/Internal/ClientCredential/ClientAssertionDelegateCredential.cs | Updates to return CredentialMaterial (JWT bearer vs JWT-PoP + optional cert). |
| src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionClientCredential.cs | Updates to return CredentialMaterial and reject mTLS mode. |
| src/client/Microsoft.Identity.Client/Internal/ClientCredential/SecretStringClientCredential.cs | Updates to return CredentialMaterial and reject mTLS mode. |
| src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs | Updates to return empty params + cert in mTLS mode, assertion params + cert in regular. |
- Rename OAuthMode to CredentialTransportProtocol (values: OAuth, Mtls) - Add 'Usually client_assertion.' to TokenRequestParameters doc - Remove null guards from CredentialMaterialResolver; credentials are responsible for never returning null (use Debug.Assert instead) - Remove pre-throw Logger.Error calls from resolver - Use _serviceBundle.Config.ClientCredential consistently in TokenClient Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
task here - #5943 I would like to get this PR merged first, if that is Ok with you @bgavrilMS |
|
Have you considered combining the CredentialMaterialResolver introduced in this PR with MtlsPopParametersInitializer? Both the classes have similar responsibiity and might be worth evaluating. Consolidating might make adding new credentials in the future maintainable. |
neha-bhargava
left a comment
There was a problem hiding this comment.
I don't think this solve the mtls flow using the cert callback
I agree these two classes have overlapping responsibility. This PR is a clean up for the tech debt I introduced. To keep the scope manageable, #5886 is tracking this consolidation |
this PR is a code cleanup/refactor of the credential material resolution pipeline. The end-to-end mTLS flow for the cert callback (WithCertificate(() => x509)) is tracked separately in #5943 and depends on the consolidation work in #5886. This PR lays the groundwork by normalizing how credentials produce material, which makes those follow-ups cleaner. |
Addresses review feedback on #5957: replaces the `is X || is Y` downcast chain in MtlsPopParametersInitializer.InitExplicitMtlsPopAsync with a single polymorphic call through the existing IClientCredential.GetCredentialMaterialAsync(Mode=Mtls) abstraction introduced in #5835. - MtlsPopParametersInitializer.cs: InitExplicitMtlsPopAsync no longer downcasts to concrete credential types. All credentials are resolved through one helper, ResolveMtlsMaterialAsync, which builds a preflight CredentialContext with Mode=Mtls. Internal InvalidCredentialMaterial errors are translated to the public MtlsCertificateNotProvided code to preserve the existing public mTLS PoP API contract. InitMtlsPopParametersAsync unchanged, preserving the IAuthenticationOperation3.AfterCredentialEvaluationAsync lifecycle from #5996. - CertificateAndClaimsClientCredential.cs: adds a _claimsToSign != null guard in the Mtls branch (preserves WithClientClaims + WithMtlsProofOfPossession rejection). ResolveCertificateAsync is now mode-aware on null cert. Drops the now-dead ResolveCertificateForMtlsAsync helper. - CredentialMatrixTests.cs: null-cert assertions split per mode; WithClaims test renamed to assert MtlsCertificateNotProvided throw. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses review feedback on #5957: replaces the `is X || is Y` downcast chain in MtlsPopParametersInitializer.InitExplicitMtlsPopAsync with a single polymorphic call through the existing IClientCredential.GetCredentialMaterialAsync(Mode=Mtls) abstraction introduced in #5835. - MtlsPopParametersInitializer.cs: InitExplicitMtlsPopAsync no longer downcasts to concrete credential types. All credentials are resolved through one helper, ResolveMtlsMaterialAsync, which builds a preflight CredentialContext with Mode=Mtls. Internal InvalidCredentialMaterial errors are translated to the public MtlsCertificateNotProvided code to preserve the existing public mTLS PoP API contract. InitMtlsPopParametersAsync unchanged, preserving the IAuthenticationOperation3.AfterCredentialEvaluationAsync lifecycle from #5996. - CertificateAndClaimsClientCredential.cs: adds a _claimsToSign != null guard in the Mtls branch (preserves WithClientClaims + WithMtlsProofOfPossession rejection). ResolveCertificateAsync is now mode-aware on null cert. Drops the now-dead ResolveCertificateForMtlsAsync helper. - CredentialMatrixTests.cs: null-cert assertions split per mode; WithClaims test renamed to assert MtlsCertificateNotProvided throw. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ateClientCredential) (#5957) * draft * fix * Resolver consolidation + Path C: single provider invocation per mTLS PoP request Consolidates ResolveCertificateForPreflightAsync into a parameterized ResolveCertificateForMtlsAsync (single resolver, optional nullErrorCode + nullErrorMessage). Adds non-AAD authority guard on TenantId computation. Plumbs preflight-resolved cert through CredentialContext.PreResolvedCertificate so credential material resolution reuses it instead of invoking the provider delegate again. Honors the single-invocation principle from issue #5943 ("a token request should generate at most 1 credential resolution"). Test now asserts providerCallCount == 1 to lock in the invariant. Removes references to #5886 (closed not_planned on 2026-05-11). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Eliminate concrete-credential downcasts in mTLS PoP initializer Addresses review feedback on #5957: replaces the `is X || is Y` downcast chain in MtlsPopParametersInitializer.InitExplicitMtlsPopAsync with a single polymorphic call through the existing IClientCredential.GetCredentialMaterialAsync(Mode=Mtls) abstraction introduced in #5835. - MtlsPopParametersInitializer.cs: InitExplicitMtlsPopAsync no longer downcasts to concrete credential types. All credentials are resolved through one helper, ResolveMtlsMaterialAsync, which builds a preflight CredentialContext with Mode=Mtls. Internal InvalidCredentialMaterial errors are translated to the public MtlsCertificateNotProvided code to preserve the existing public mTLS PoP API contract. InitMtlsPopParametersAsync unchanged, preserving the IAuthenticationOperation3.AfterCredentialEvaluationAsync lifecycle from #5996. - CertificateAndClaimsClientCredential.cs: adds a _claimsToSign != null guard in the Mtls branch (preserves WithClientClaims + WithMtlsProofOfPossession rejection). ResolveCertificateAsync is now mode-aware on null cert. Drops the now-dead ResolveCertificateForMtlsAsync helper. - CredentialMatrixTests.cs: null-cert assertions split per mode; WithClaims test renamed to assert MtlsCertificateNotProvided throw. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * address pr comments * Address PR review (#5957): document mTLS invariant + fix WithClientClaims error message - CredentialMaterialResolver / CertificateAndClaimsClientCredential: Document the single-invocation invariant (issue #5943) at both the resolver short-circuit site and inside the credential, so future CertificateAndClaimsClientCredential subclasses understand they must keep mTLS-mode output equal to (empty, cert) — overriding the method alone will be silently bypassed by the resolver at runtime. Addresses Robbie's review comment #1 (Option 2). - MtlsPopParametersInitializer.cs: Correct the Case 1 comment in TryInitImplicitBearerOverMtlsAsync — the polymorphic resolve is reachable only because ConfidentialClientApplicationBuilder.Validate rejects the non-cert pairing at construction time; there is no Bearer-path fallback. Addresses Robbie's review comment #2. - CertificateAndClaimsClientCredential / MsalErrorMessage: When mTLS PoP is combined with WithClientClaims, throw the existing MsalError.MtlsCertificateNotProvided (unchanged code, preserves MtlsPopWithoutCertificateWithClientClaimsAsync) with a new MsalErrorMessage.MtlsPopNotSupportedWithClientClaimsMessage that describes the actual WithClientClaims + mTLS conflict instead of the misleading "callback returned null" string. Addresses Robbie's review comment #3 (Option 2). Full local unit test suite: 2055/2055 passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address Robbie's round-2 review (PR #5957) Fixes 5 follow-up review comments after fdff674 (squashed in rebase as 7af3ea0). One real diagnostic bug + one regression test + doc tightening: * MsalErrorMessage.cs: - Renamed MtlsPopNotSupportedWithClientClaimsMessage to MtlsNotSupportedWithClientClaimsMessage and generalized the wording. The guard that uses this message fires for *every* mTLS-mode resolution path — explicit .WithMtlsProofOfPossession() AND implicit CertificateOptions.SendCertificateOverMtls = true. The old wording falsely blamed Proof-of-Possession even when the user only opted into Bearer-over-mTLS, producing a confusing diagnostic. * CertificateAndClaimsClientCredential.cs: - Updated reference to the renamed constant. - Tightened the in-line comment above the guard to call out that the rejection fires for both explicit PoP and implicit Bearer-over-mTLS, so the message wording is intentional. * MtlsPopParametersInitializer.cs: - Added the 4th sub-bullet to the <remarks> on InitExplicitMtlsPopAsync covering the WithClientClaims direct-throw path through CertificateAndClaimsClientCredential.GetCredentialMaterialAsync. The list now describes all four GetCredentialMaterialAsync(Mtls) outcomes: cert credentials, signed-assertion credentials, client-claims (direct throw), and unsupported credentials (translated via the catch). * MtlsPopTests.cs: - Tightened MtlsPopWithoutCertificateWithClientClaimsAsync to assert StringAssert.Contains(ex.Message, "WithClientClaims"). A future error-message centralisation refactor cannot silently revert the diagnostic without failing this test. - Added SendCertificateOverMtls_WithClientClaims_ThrowsClearMessageAsync as a regression test for the previously-uncovered combo: WithCertificate(cert, CertificateOptions { SendCertificateOverMtls = true }) + WithClientClaims(cert, claims), *no* .WithMtlsProofOfPossession(). The new test asserts the message names both transports and the WithClientClaims API so the diagnostic stays actionable for the Bearer-over-mTLS path. Build: 0 warnings, 0 errors, 4 TFMs. Test (Microsoft.Identity.Test.Unit, net8.0): 2119/2119 passed, 19 skipped (platform-gated). Includes the new regression test and the tightened existing test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What does this PR do?
This PR refactors client credential handling to separate credential material generation from token request application, while also addressing all review feedback from #5748.
Main changes
Credential material refactor
ClientCredentialApplicationResult/SecretStringClientCredentialflow with:CredentialMaterial— immutable output (body params + optional certificate)CredentialContext— immutable input bundle (endpoint, mode, authority, tenant, logger)CredentialMaterialResolver— static adapter betweenAuthenticationRequestParametersand credentialsOAuthMode— enum replacing boolean flags (RegularvsMtlsMode)ClientSecretCredential— renamed fromSecretStringClientCredentialIClientCredentialto returnCredentialMaterialthroughGetCredentialMaterialAsync(CredentialContext, CancellationToken).TokenClient update
CredentialContext.TokenEndpoint.CredentialMaterialResolver.ResolveAsync()withLogBlockDurationtiming.Credential implementations
Updated all credential implementations to follow the new model:
client_secret; rejects mTLS withInvalidCredentialMaterialmTLS / PoP handling
OAuthModeenum instead of boolean flags.MsalClientExceptionMsalClientExceptionMsalClientExceptionClientSignedAssertionwithTokenBindingCertificatefor JWT-PoP.Exception taxonomy
InvalidOperationExceptionMsalClientExceptionwithInvalidCredentialMaterialTokenRequestParametersinCredentialMaterial→ArgumentNullExceptionPublic surface
MsalError.InvalidCredentialMaterialHow this addresses #5748 feedback
Every open thread from #5748 was reviewed and carried forward:
MtlsValidationContextclass (neha-bhargava)CredentialContextOAuthModeenumrequestContextnaming (neha-bhargava, bgavrilMS)CredentialContextis the single typeMtlsCertificate/IsMtlsPopRequestedResolvedCertificate— not MTLS-specificCredentialMaterialhas onlyTokenRequestParameters+ResolvedCertificateCredentialSourceenum (bgavrilMS)CancellationTokenstored on context (bgavrilMS)AzureRegion/AuthorityTypeon context (bgavrilMS)InvalidOperationExceptionfor internal invariantsCredentialMaterialResolverSecretStringClientCredentialnaming (bgavrilMS)ClientSecretCredentialCollectionHelpers.GetEmptyDictionaryLogBlockDuration(bgavrilMS)Row1bandRow2btestsAssertionRequestOptionsmissingAuthority/TenantId(copilot-reviewer)CredentialContextand propagated to all callback optionsTests
CredentialMatrixTestscovers the full credential×mode matrix (10 rows + edge cases):Why
This change makes credential resolution more explicit and easier to reason about by:
OAuthModeenum