Add FIC two-leg exchange over mTLS Proof-of-Possession (follow-up to #938) - #939
Open
Robbie-Microsoft wants to merge 5 commits into
Open
Add FIC two-leg exchange over mTLS Proof-of-Possession (follow-up to #938)#939Robbie-Microsoft wants to merge 5 commits into
Robbie-Microsoft wants to merge 5 commits into
Conversation
Contributor
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.
Robbie-Microsoft
force-pushed
the
rginsburg/sni-mtls-pop
branch
from
July 7, 2026 22:31
5339d9e to
dd4f13a
Compare
Robbie-Microsoft
force-pushed
the
rginsburg/sni-mtls-pop-fic
branch
5 times, most recently
from
July 8, 2026 21:58
772ef58 to
614acc3
Compare
Contributor
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>
Robbie-Microsoft
force-pushed
the
rginsburg/sni-mtls-pop-fic
branch
from
July 8, 2026 22:51
614acc3 to
acd78ed
Compare
Robbie-Microsoft
added a commit
that referenced
this pull request
Jul 30, 2026
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>
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.
Summary
Follow-up to #938. Adds the optional two-leg Federated Identity Credential (FIC) exchange over mTLS PoP: the leg-2 confidential client presents the leg-1 (cert-bound
mtls_pop) token as ajwt-popclient assertion over an mTLS connection bound by the same certificate, to obtain the final token.Usage
What changed (delta over #938)
msal/oauth2cli/oauth2.py—jwt-popclient-assertion-type constant.msal/application.py— detect themtls_binding_certificatecredential sub-key (FIC leg 2); carry the leg-1 assertion asjwt-pop; route every leg-2 request over the mTLS endpoint (even a plain Bearer travels the cert-bound connection); reject a leg-2 app on non–client-credential / silent flows and reject a string assertion without a binding cert; FIC-aware error messages.test_application.py; a self-skipping E2E case intest_e2e.py. Docstrings +docs/index.rstFIC bullet;fic_two_leg_over_mtls_pop()sample.Full unit suite passes (base + FIC); no regressions.