Skip to content

Add FIC two-leg exchange over mTLS Proof-of-Possession (follow-up to #938)#939

Open
Robbie-Microsoft wants to merge 3 commits into
rginsburg/sni-mtls-popfrom
rginsburg/sni-mtls-pop-fic
Open

Add FIC two-leg exchange over mTLS Proof-of-Possession (follow-up to #938)#939
Robbie-Microsoft wants to merge 3 commits into
rginsburg/sni-mtls-popfrom
rginsburg/sni-mtls-pop-fic

Conversation

@Robbie-Microsoft

@Robbie-Microsoft Robbie-Microsoft commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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 a jwt-pop client assertion over an mTLS connection bound by the same certificate, to obtain the final token.

Stacked on rginsburg/sni-mtls-pop (#938) — review/merge #938 first; GitHub auto-retargets this PR to dev once #938 merges.

Usage

# Leg 1: SN/I cert -> cert-bound mtls_pop token (see #938)
leg1 = leg1_app.acquire_token_for_client([exchange_scope], mtls_proof_of_possession=True)

# Leg 2: present the leg-1 token as a jwt-pop assertion over the same cert's mTLS connection
leg2_app = msal.ConfidentialClientApplication(
    client_id,
    authority="https://login.microsoftonline.com/<tenant-id>",
    client_credential={
        "client_assertion": leg1["access_token"],   # cert-bound leg-1 token
        "mtls_binding_certificate": {"private_key_pfx_path": "sni.pfx", "public_certificate": True},
    },
)
leg2 = leg2_app.acquire_token_for_client(scopes, mtls_proof_of_possession=True)

What changed (delta over #938)

  • msal/oauth2cli/oauth2.pyjwt-pop client-assertion-type constant.
  • msal/application.py — detect the mtls_binding_certificate credential sub-key (FIC leg 2); carry the leg-1 assertion as jwt-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.
  • Tests — FIC leg-2 (PoP + Bearer-over-mTLS) and the fail-fast cases in test_application.py; a self-skipping E2E case in test_e2e.py. Docstrings + docs/index.rst FIC bullet; fic_two_leg_over_mtls_pop() sample.

Full unit suite passes (base + FIC); no regressions.

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

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-pop client assertion type constant and uses it for FIC leg-2 requests.
  • Extends ConfidentialClientApplication to detect client_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.

Comment thread msal/application.py
Comment thread msal/application.py Outdated
@Robbie-Microsoft Robbie-Microsoft force-pushed the rginsburg/sni-mtls-pop branch from 5339d9e to dd4f13a Compare July 7, 2026 22:31
@Robbie-Microsoft Robbie-Microsoft force-pushed the rginsburg/sni-mtls-pop-fic branch 5 times, most recently from 772ef58 to 614acc3 Compare July 8, 2026 21:58
@Robbie-Microsoft Robbie-Microsoft requested a review from Copilot July 8, 2026 22:26

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 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/bytes client_assertion. This makes the documented/expected FIC usage noisy (and can fail callers running with warnings-as-errors). Consider suppressing this warning when mtls_binding_certificate is 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 "

Robbie-Microsoft and others added 3 commits July 8, 2026 18:51
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 Robbie-Microsoft force-pushed the rginsburg/sni-mtls-pop-fic branch from 614acc3 to acd78ed Compare July 8, 2026 22:51
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.

2 participants