Skip to content

Refactor client credential material resolution#5835

Merged
gladjohn merged 12 commits into
mainfrom
gladjohn/refactor
Apr 27, 2026
Merged

Refactor client credential material resolution#5835
gladjohn merged 12 commits into
mainfrom
gladjohn/refactor

Conversation

@gladjohn

@gladjohn gladjohn commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

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

  • Replaced the old ClientCredentialApplicationResult / SecretStringClientCredential flow with:
    • CredentialMaterial — immutable output (body params + optional certificate)
    • CredentialContext — immutable input bundle (endpoint, mode, authority, tenant, logger)
    • CredentialMaterialResolver — static adapter between AuthenticationRequestParameters and credentials
    • OAuthMode — enum replacing boolean flags (Regular vs MtlsMode)
    • ClientSecretCredential — renamed from SecretStringClientCredential
  • Updated IClientCredential to return CredentialMaterial through GetCredentialMaterialAsync(CredentialContext, CancellationToken).

TokenClient update

  • Token endpoint is resolved once and passed through credential resolution via CredentialContext.TokenEndpoint.
  • Credential material is applied via CredentialMaterialResolver.ResolveAsync() with LogBlockDuration timing.

Credential implementations

Updated all credential implementations to follow the new model:

  • CertificateAndClaimsClientCredential — Regular: JWT-bearer + cert; MtlsMode: empty params + cert
  • ClientSecretCredential — Returns client_secret; rejects mTLS with InvalidCredentialMaterial
  • SignedAssertionClientCredential — Returns JWT-bearer; rejects mTLS
  • ClientAssertionStringDelegateCredential — Returns JWT-bearer from callback; rejects mTLS
  • ClientAssertionDelegateCredential — Returns JWT-bearer or JWT-PoP depending on cert presence; supports mTLS

mTLS / PoP handling

  • Mode determination uses OAuthMode enum instead of boolean flags.
  • Invalid combinations enforced per-credential:
    • secret + mTLS → MsalClientException
    • static signed assertion + mTLS → MsalClientException
    • string-returning assertion delegate + mTLS → MsalClientException
  • Supported: delegate-returned ClientSignedAssertion with TokenBindingCertificate for JWT-PoP.

Exception taxonomy

  • Internal invariants (null material, null params) → InvalidOperationException
  • Unsupported credential/mode combinations → MsalClientException with InvalidCredentialMaterial
  • Null TokenRequestParameters in CredentialMaterialArgumentNullException

Public surface

  • Added: MsalError.InvalidCredentialMaterial

How this addresses #5748 feedback

Every open thread from #5748 was reviewed and carried forward:

#5748 Feedback Resolution
Separate MtlsValidationContext class (neha-bhargava) Consolidated into CredentialContext
String-based mode field (neha-bhargava) Replaced with OAuthMode enum
Confusing requestContext naming (neha-bhargava, bgavrilMS) CredentialContext is the single type
Flag-based mode evaluation in TokenClient (neha-bhargava) Mode determined at resolution time from MtlsCertificate/IsMtlsPopRequested
Cert naming MTLS-specific (neha-bhargava, bgavrilMS) ResolvedCertificate — not MTLS-specific
Cert resolution logging removed (neha-bhargava) Restored with concise verbose logging
Dead metadata fields (neha-bhargava, bgavrilMS) Removed — CredentialMaterial has only TokenRequestParameters + ResolvedCertificate
Hash-prefix field (bgavrilMS) Removed
CredentialSource enum (bgavrilMS) Deleted entirely
CancellationToken stored on context (bgavrilMS) Passed explicitly as last async parameter
Unused AzureRegion/AuthorityType on context (bgavrilMS) Removed
Wrong exception type for internal bugs (bgavrilMS) InvalidOperationException for internal invariants
Orchestrator naming (bgavrilMS) Renamed to CredentialMaterialResolver
SecretStringClientCredential naming (bgavrilMS) Renamed to ClientSecretCredential
"Overusing logger.Error" pre-throw (bgavrilMS) Removed pre-throw Error calls; top-level catch handles logging
Error messages suggest mTLS = mTLS POP only (bgavrilMS) Rephrased to "over mTLS" for broader scenarios
Compute assertion type once (bgavrilMS) Computed once and reused
Empty collections (bgavrilMS) Uses CollectionHelpers.GetEmptyDictionary
Use LogBlockDuration (bgavrilMS) Added for credential resolution block
Missing dynamic cert tests (bgavrilMS) Added Row1b and Row2b tests
AssertionRequestOptions missing Authority/TenantId (copilot-reviewer) Added to CredentialContext and propagated to all callback options

Tests

  • CredentialMatrixTests covers the full credential×mode matrix (10 rows + edge cases):
    • Rows 1–2: Certificate (static + dynamic) × Regular/MtlsMode
    • Rows 3–4: Secret × Regular/MtlsMode (unsupported)
    • Rows 5–6: Static signed assertion × Regular/MtlsMode (unsupported)
    • Rows 7–8: String callback × Regular/MtlsMode (unsupported)
    • Rows 9–10: Delegate+cert callback × Regular/MtlsMode
    • Edge cases: null cert, empty assertion, null assertion, null material, empty params
    • Authority/TenantId propagation: verified across all 3 callback-backed credential types
  • 21 tests, all passing

Why

This change makes credential resolution more explicit and easier to reason about by:

@gladjohn gladjohn requested a review from a team as a code owner March 10, 2026 21:41
Copilot AI review requested due to automatic review settings March 10, 2026 21:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and ClientAuthMode; updates IClientCredential to return CredentialMaterial.
  • Updates TokenClient to resolve the token endpoint once and pass it through credential material resolution, then apply returned body parameters and resolved certificate.
  • Adds MsalError.InvalidCredentialMaterial and adds unit coverage via CredentialMatrixTests.

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.

Comment thread tests/Microsoft.Identity.Test.Unit/RequestsTests/CredentialMatrixTests.cs Outdated
Copilot AI review requested due to automatic review settings March 10, 2026 22:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 4 comments.

Comment thread src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs Outdated
Comment thread src/client/Microsoft.Identity.Client/Internal/ClientCredential/ClientAuthMode.cs Outdated
Comment thread src/client/Microsoft.Identity.Client/Internal/ClientCredential/OAuthMode.cs Outdated
Comment thread src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs Outdated
Copilot AI review requested due to automatic review settings March 24, 2026 15:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 2 comments.

Comment thread src/client/Microsoft.Identity.Client/Internal/ClientCredential/OAuthMode.cs Outdated
- 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>
@gladjohn gladjohn requested a review from bgavrilMS April 22, 2026 13:04
@gladjohn

Copy link
Copy Markdown
Contributor Author

Does the PR add support for mTLS POP for the WithCertificate( () => x509) ? If not, I think you should create a feature branch. We need to understand if the approach is viable for the new cred.

task here - #5943

I would like to get this PR merged first, if that is Ok with you @bgavrilMS

Comment thread src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs
@neha-bhargava

Copy link
Copy Markdown
Contributor

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 neha-bhargava left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this solve the mtls flow using the cert callback

@gladjohn

gladjohn commented Apr 26, 2026

Copy link
Copy Markdown
Contributor Author

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.

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

@gladjohn

Copy link
Copy Markdown
Contributor Author

I don't think this solve the mtls flow using the cert callback

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.

Copilot AI review requested due to automatic review settings April 26, 2026 14:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 1 comment.

@gladjohn gladjohn enabled auto-merge (squash) April 27, 2026 18:30
@gladjohn gladjohn merged commit 02d435d into main Apr 27, 2026
15 checks passed
@gladjohn gladjohn deleted the gladjohn/refactor branch April 27, 2026 18:43
gladjohn added a commit that referenced this pull request May 14, 2026
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>
gladjohn added a commit that referenced this pull request Jun 1, 2026
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>
gladjohn added a commit that referenced this pull request Jun 2, 2026
…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>
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.

4 participants