Skip to content

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

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

Robbie-Microsoft wants to merge 16 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 developer-orchestrated two-leg Federated Identity Credential (FIC) exchange over mTLS:

  1. Leg 1 acquires a certificate-bound mtls_pop token for the token-exchange audience.
  2. Leg 2 receives that assertion together with the same certificate, sends it as a jwt-pop client assertion over mTLS, and acquires either a final mtls_pop token or an ordinary Bearer token.

Targets rginsburg/sni-mtls-pop (#938). Review and merge #938 first.

Developer experience

cert = {
    "private_key_pfx_path": "sni.pfx",
    "public_certificate": True,
}

leg1_app = msal.ConfidentialClientApplication(
    leg1_client_id,
    authority=leg1_authority,
    client_credential=cert,
)


def get_federated_assertion():
    result = leg1_app.acquire_token_for_client(
        ["api://AzureADTokenExchange/.default"],
        mtls_proof_of_possession=True,
    )
    return result["access_token"]


leg2_app = msal.ConfidentialClientApplication(
    leg2_client_id,
    authority=leg2_authority,
    client_credential={
        "client_assertion": get_federated_assertion,
        "mtls_binding_certificate": cert,
    },
)

# Final certificate-bound resource token
result = leg2_app.acquire_token_for_client(
    ["https://graph.microsoft.com/.default"],
    mtls_proof_of_possession=True,
)

The callable keeps assertion renewal behind the leg-1 cache instead of pinning an expiring assertion string.

Output matrix

Leg-2 request Assertion and transport Final token Result binding context Resource usage
mtls_proof_of_possession=True jwt-pop over mTLS with the leg-1 certificate mtls_pop Present and matches the leg-1 certificate Present the returned context to the Graph mTLS endpoint
No final PoP request jwt-pop over mTLS with the leg-1 certificate Bearer Absent Use as an ordinary Bearer token; no Graph-over-mTLS call

The certificate remains essential to authenticate the bound assertion in both rows. It is exposed as binding_certificate only when the final access token is itself certificate-bound.

Implementation

  • Adds the jwt-pop client-assertion type.
  • Adds the mtls_binding_certificate credential sub-key for the leg-2 assertion flow.
  • Routes every supported leg-2 request through the mTLS token endpoint and keeps the assertion/certificate pairing intact.
  • Requests and cache-partitions the final token by certificate only when mtls_proof_of_possession=True.
  • Rejects unsupported non-client-credential flows, missing/invalid binding certificates, custom HTTP transports, and per-request transport overrides.
  • Keeps final Bearer tokens on the ordinary cache contract and does not expose a downstream binding context.

Validation

  • Unit coverage for jwt-pop wire parameters, mTLS endpoint selection, final PoP and Bearer result contracts, assertion renewal, cache behavior, invalid credentials, and unsupported flows.
  • Live E2E coverage for both final outputs:
    • Final mtls_pop: validates binding-certificate continuity and calls the Microsoft Graph mTLS endpoint.
    • Final Bearer: requests the Storage audience and verifies that the result is unbound and has no binding_certificate.
  • Focused local validation: 119 tests passed, including 43 subtests.

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 Outdated
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
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>
Robbie-Microsoft and others added 2 commits July 30, 2026 16:44
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>
@bgavrilMS

Copy link
Copy Markdown
Member

Pls mark PRs that should not be looked at as draft or close them.

Robbie-Microsoft and others added 3 commits October 9, 2026 16:44
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>
@Robbie-Microsoft
Robbie-Microsoft marked this pull request as ready for review October 22, 2026 12:55

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.

🔵 Needs a closer look

The certificate-bound, cross-application exchange needs human security review and live validation of both final-token modes.

No open issues 🎉

Review effort: Balanced

Robbie-Microsoft and others added 4 commits October 22, 2026 17:26
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>
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>

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.

🔵 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

Comment thread sample/confidential_client_mtls_pop_sample.py Outdated
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>

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.

🔵 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

Robbie-Microsoft and others added 3 commits October 23, 2026 15:12
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>

This branch has not been deployed

No deployments
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.

3 participants