Repository navigation
Add FIC two-leg exchange over mTLS Proof-of-Possession (follow-up to #938) - #939
Robbie-Microsoft wants to merge 16 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for the optional two-leg Federated Identity Credential (FIC) token exchange over mTLS Proof-of-Possession, where a leg-1 mtls_pop token is presented as a jwt-pop client assertion over an mTLS connection bound to the same certificate to obtain the final token.
Changes:
- Introduces the
jwt-popclient assertion type constant and uses it for FIC leg-2 requests. - Extends
ConfidentialClientApplicationto detectclient_credential["mtls_binding_certificate"](FIC leg 2) and route leg-2 traffic over the mTLS token endpoint (including implicit Bearer-over-mTLS). - Adds unit/E2E coverage, docs, and a gated sample for the two-leg flow.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
msal/oauth2cli/oauth2.py |
Adds jwt-pop client assertion type constant for OAuth2 client assertions. |
msal/application.py |
Implements FIC leg-2 detection and mTLS routing; sets client_assertion_type to jwt-pop for leg-2. |
tests/test_application.py |
Adds unit tests validating jwt-pop assertion type and Bearer-over-mTLS behavior for FIC leg-2. |
tests/test_e2e.py |
Adds an E2E test for the two-leg FIC exchange over mTLS PoP (self-skipping when not configured). |
docs/index.rst |
Documents the FIC leg-2 mtls_binding_certificate configuration and jwt-pop behavior. |
sample/confidential_client_mtls_pop_sample.py |
Adds a gated sample function demonstrating the two-leg FIC flow over mTLS PoP. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
5339d9e to
dd4f13a
Compare
772ef58 to
614acc3
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
msal/application.py:892
- The new FIC leg-2 configuration requires passing a leg-1 access token as a string
client_assertion, but the constructor currently emits a DeprecationWarning for any static string/bytesclient_assertion. This makes the documented/expected FIC usage noisy (and can fail callers running with warnings-as-errors). Consider suppressing this warning whenmtls_binding_certificateis present (FIC leg 2).
# Warn if using a static string/bytes client_assertion (discouraged for long-running apps)
if isinstance(client_credential, dict) and isinstance(
client_credential.get("client_assertion"), (str, bytes)):
warnings.warn(
"Passing a static string/bytes 'client_assertion' is "
Stacked on top of the SN/I mTLS PoP base support. Enables a Federated Identity Credential (FIC) two-leg exchange over mutual-TLS: a leg-1 cert-bound mtls_pop token is presented as a jwt-pop client assertion over an mTLS connection bound by the same certificate to obtain the final token. - Detect the client_credential mtls_binding_certificate sub-key (FIC leg 2) and carry the leg-1 assertion as a jwt-pop client assertion over mTLS - Route every FIC leg-2 request over the mTLS endpoint, so even a Bearer token travels the cert-bound connection (implicit Bearer-over-mTLS) - Add unit + E2E coverage, docs, and a two-leg sample Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A confidential client configured for a Federated Identity Credential (FIC) leg-2 exchange (a client_assertion plus an mtls_binding_certificate) is only meaningful through acquire_token_for_client, which presents the binding certificate over mutual-TLS and carries the leg-1 assertion as jwt-pop. If such an app is instead used with any other flow (authorization code, on-behalf-of, refresh token, username/password, or user-FIC), MSAL would send that cert-bound leg-1 assertion as an ordinary jwt-bearer over a non-mTLS connection. ESTS rejects it, but with a confusing server error. Add a small _reject_if_mtls_binding_cert guard and call it at the top of those flows so the misuse fails fast with a clear ValueError that points the caller to acquire_token_for_client. The guard is a no-op for every non-FIC-leg-2 app, so public clients and ordinary confidential clients are unaffected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The _reject_if_mtls_binding_cert guard already covered the six non-client-credential acquisition flows, but not acquire_token_silent / acquire_token_silent_with_error. With a shared token cache holding user refresh tokens, a FIC leg-2 app (client_assertion + mtls_binding_certificate) could silently redeem one of those RTs, putting the cert-bound leg-1 assertion on a non-mTLS jwt-bearer request -- which ESTS rejects with a confusing invalid_client rather than a clear message. Add the guard to both public silent entry points. Both call the private _acquire_token_silent_with_error directly (not each other), and acquire_token_for_client also uses that private helper, so the guard lives on the two public wrappers -- never on the private method -- to keep the legitimate mTLS client-credential flow working. No-op for normal apps. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
614acc3 to
acd78ed
Compare
Per cross-SDK review: document that the FIC two-leg e2e (PR #939) reuses this helper as-is, so it is not inlined later. Explains why it takes an explicit cert_credential instead of reading self.app's cert. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Bring the Task-1 SNI hardening from rginsburg/sni-mtls-pop (tip b9233c6) into the FIC follow-up branch: the reusable _call_graph_with_mtls_pop_token resource-call e2e helper, the cross-SDK naming matrix, the loud-fail on invalid_request, and the region split (drop the regional mtls_pop cell that intermittently downgrades to Bearer; make the Bearer cell regional westus3; keep the pop cell global). Conflicts resolved by keeping both sides: - docs/index.rst: retain the SNI note that only the mTLS-PoP path drops the signed client_assertion AND the FIC leg-2 configuration note. - msal/application.py: retain the FIC leg-2 jwt-pop assertion branch AND fold the SNI clarifying comment into the vanilla SN/I else path. The FIC e2e test and its leg-1 skip-hatch are left untouched; porting the resource-call helper into the FIC e2e is Task 2. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Bring the FIC branch current with base dev by merging the resync'd SNI branch (rginsburg/sni-mtls-pop @ 4ec592f, which merged origin/dev), so #939 also carries PRs #911, #937, #940, #942. Single conflict in msal/application.py acquire_token_for_client(), resolved by integrating BOTH sides: keep the FIC-aware mTLS condition (mtls_proof_of_possession or self._mtls_is_fic_leg2) and its comment from this branch, AND add dev's forwarded_client_claims request-setup block above it. The private _acquire_token_for_client() auto-merged correctly, combining the FIC/mtls client selection (is_mtls_pop or self._mtls_is_fic_leg2) with dev's client_claims -> OAuth claims merge. The FIC e2e test test_fic_two_leg_over_mtls_pop and its leg-1 skip-hatch are left UNTOUCHED (de-hatch + resource-call port + Credential_Fic_Output_* naming remain Task 2). Region split, x509 pop/bearer matrix, and downgrade fail-closed are all preserved. Verified: py_compile clean; e2e matrix (pop/bearer) + FIC test collect; 22 mtls_pop + forwarded_client_claims unit tests pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Pls mark PRs that should not be looked at as draft or close them. |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Return the binding context for both FIC final token modes and use it for downstream mTLS resource validation.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…nsburg/sni-mtls-pop-fic
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Preserve FIC Bearer responses while integrating parent cache, transport, and telemetry fixes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The security-sensitive assertion and certificate-binding changes require final human validation alongside prerequisite #938.
1 open issue
Review effort: Balanced
Validate final token types before returning or caching responses, reject unsupported FIC outputs, validate credentials before client construction, and guard the sample resource call. Cover cache isolation and same-scope assertion renewal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The security-sensitive assertion, transport, and cache integration needs final human review after prerequisite #938 is merged.
No open issues 🎉
1 resolved since last review
Review effort: Balanced
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adapt FIC scenarios to the shared certificate configuration and preserve global PoP and regional Bearer parity. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Summary
Follow-up to #938. Adds the developer-orchestrated two-leg Federated Identity Credential (FIC) exchange over mTLS:
mtls_poptoken for the token-exchange audience.jwt-popclient assertion over mTLS, and acquires either a finalmtls_poptoken or an ordinary Bearer token.Developer experience
The callable keeps assertion renewal behind the leg-1 cache instead of pinning an expiring assertion string.
Output matrix
mtls_proof_of_possession=Truejwt-popover mTLS with the leg-1 certificatemtls_popjwt-popover mTLS with the leg-1 certificateBearerThe certificate remains essential to authenticate the bound assertion in both rows. It is exposed as
binding_certificateonly when the final access token is itself certificate-bound.Implementation
jwt-popclient-assertion type.mtls_binding_certificatecredential sub-key for the leg-2 assertion flow.mtls_proof_of_possession=True.Validation
jwt-popwire parameters, mTLS endpoint selection, final PoP and Bearer result contracts, assertion renewal, cache behavior, invalid credentials, and unsupported flows.mtls_pop: validates binding-certificate continuity and calls the Microsoft Graph mTLS endpoint.binding_certificate.