feat(auth): add JWT-trust verification path with claims-derived identity - #6746
Open
jonpspri wants to merge 4 commits into
Open
feat(auth): add JWT-trust verification path with claims-derived identity#6746jonpspri wants to merge 4 commits into
jonpspri wants to merge 4 commits into
Conversation
jonpspri
added this pull request to stack #6729
September 10, 2026 01:36
jonpspri
marked this pull request as ready for review
September 10, 2026 01:36
jonpspri
requested review from
Lang-Akshay,
brian-hussey,
crivetimihai,
ja8zyjits and
msureshkumar88
as code owners
September 10, 2026 01:36
jonpspri
force-pushed
the
feat/5900-trust-branch
branch
from
September 10, 2026 06:23
483b589 to
49a65bb
Compare
jonpspri
removed this pull request from stack #6729
September 12, 2026 08:50
jonpspri
force-pushed
the
feat/5900-trust-branch
branch
from
September 12, 2026 09:07
49a65bb to
5e67afe
Compare
jonpspri
added this pull request to stack #6798
September 12, 2026 09:08
jonpspri
force-pushed
the
feat/5900-trust-branch
branch
from
September 12, 2026 09:20
5e67afe to
660b025
Compare
jonpspri
force-pushed
the
feat/5900-trust-branch
branch
from
September 12, 2026 09:48
660b025 to
de08702
Compare
jonpspri
force-pushed
the
feat/5900-trust-branch
branch
from
September 12, 2026 16:45
72b540c to
36cd3c5
Compare
jonpspri
force-pushed
the
feat/5900-trust-branch
branch
from
September 12, 2026 17:20
36cd3c5 to
a68f67d
Compare
jonpspri
force-pushed
the
feat/5900-trust-branch
branch
from
September 12, 2026 17:35
a68f67d to
671e292
Compare
jonpspri
force-pushed
the
feat/5900-trust-branch
branch
from
September 12, 2026 17:54
671e292 to
58e2687
Compare
jonpspri
force-pushed
the
feat/5900-trust-branch
branch
2 times, most recently
from
September 12, 2026 18:33
dde2d02 to
fd192e0
Compare
Wire the trust-eligible branch in get_current_user per the #5896 dispatch rule: token_use="trusted" with trust mode ON authenticates from mapped claims via extract_trusted_principal; the configured revocation claim is checked with _check_token_revoked_sync; user lookup, is_active, the UUID->email seam, and DB team resolution are skipped. A marked token with trust mode OFF gets 401 and never enters the default funnel. Entra group-overage markers dispatch on jwt_trust_overage_policy (#5977). Extra scope per the work order: - AuthCache Redis keys gain the auth-mode segment (mcpgw:auth:{version}:{mode}:{key_type}:{identifier}); a mode flip cold-starts the cache. The A.6 prefix test and the doctest are updated. - tests/helpers/auth.py gains make_trusted_test_jwt. - B.1 matrix branch-(a) rows and the WO-B.4/B.5 strict-xfail tests flip to green; only the branch-(b) row stays strict-xfail with a pointer to #5903. - New smoke suite asserts zero email_users reads on the trust path. - Live-gateway test file (e2e, skip_no_gateway) covers both modes. - configuration.md documents the accepted posture change: no per-user is_active kill-switch in trust mode. Signed-off-by: Jonathan Springer <jps@s390x.com>
Add tests/live_gateway/test_trust_mode_entra_barrier.py next to the existing trust-mode RBAC live checks. It documents, with a pinned assertion, that a genuine Entra-signed end-user access token sent to POST /a2a/<agent>/invoke is rejected with 401 at the authentication choke point before external verification, group mapping, RBAC, visibility, or agent lookup can run: get_current_user() calls the internal-only verify_jwt_token_cached() and never dispatches external issuers to the JWKS path in verify_credentials_cached(). The forged-token control asserts 401 in every mode. Both tests self-skip without a reachable gateway, without the untracked token files (ENTRA_BARRIER_TOKEN_DIR), and the pinned non-compliance test additionally requires JWT_TRUST_MODE=jwt-trust. Token material is never logged. When the ingress is wired to the external-aware verifier, the valid-token expectation flips to 404 (deliberately nonexistent agent); the docstring marks that flip point. Verified live against a standalone trust-mode gateway: 2 passed with tokens; per-guard skips verified for no-gateway, no-token-files, and db-mode runs. Signed-off-by: Jonathan Springer <jps@s390x.com>
get_current_user_with_permissions dropped the trust principal's mapped role names and claims-derived admin flag: only email, is_admin, teams, and user_id survived into the context dict. Carry them so the permission layer can consume claims identity (#5900, F2 first half): - Trust path (VirtualPrincipal, token_use="trusted"): the context gains roles (mapped role names, default []) and token_is_admin (the claims-derived admin flag, default False), both sourced from the principal; token_teams falls back to the principal's mapped teams when request.state was not populated (the live trust branch in get_current_user always sets it, so the fallback is a no-op there). - Non-trust returns (internal JWT, proxy, anonymous, disabled) gain the same keys with neutral defaults (roles=[], token_is_admin=False) so downstream consumers can rely on key presence. token_teams is added where previously absent with the value downstream .get() already produced ([] for anonymous; None for admin-capable paths, preserving the None=unrestricted/[]=public-only scoping semantic). - DB-derived is_admin never leaks into token_is_admin on non-trust paths. No permission derivation happens on this branch: role names are only carried. The single name-resolution point remains extract_trusted_principal, which validates each mapping-supplied role name scope-exactly via mcpgateway.services.role_resolution.resolve_mapping_role (single active row, team>global, lowest id, never unioned) per the NB6 rule. The Entra A2A ingress barrier stays pinned at 401; ingress is a separate up-stack task. Signed-off-by: Jonathan Springer <jps@s390x.com>
The claims-identity change dropped the pre-existing token_use entry from get_current_user_with_permissions' authenticated return dict. Two live consumers read it from exactly this dict: _resolve_team_and_check_mode derives the accessed resource's team only for session/api tokens (without the key it falls through to check_any_team=True, aggregating roles across all teams and broadening mutate permissions cross-team), and the OAuth router's _build_user_context branches on token_use == "session" for DB-authoritative vault path selection. Restore the key alongside the additive roles/token_is_admin entries and pin it with regression tests: a session-type context carries token_use plus the neutral claims keys, and _resolve_team_and_check_mode derives the resource team for a session context while broadening to any-team without it. Signed-off-by: Jonathan Springer <jps@s390x.com>
jonpspri
force-pushed
the
feat/5900-trust-branch
branch
from
September 12, 2026 21:11
e18d043 to
0a27c27
Compare
Collaborator
Author
|
Requirement note (remediation) — context keys The trust-path context now carries Review caught one regression: the patch first dropped the The barrier test in this PR was re-pinned upstack (PR #6750). A valid Entra token with no seeded trust root keeps 401 (fall-through). See the notes on #5900. |
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.
Wires the trust-eligible branch into
get_current_userper the #5896 dispatch rule. A token markedtoken_use="trusted"with trust mode OFF is rejected 401 before the default funnel — the UUID heuristic could otherwise re-attribute identity. With trust mode ON, gateway-signed trust tokens authenticate viaextract_trusted_principal: identity, teams, and roles come from the mapped claims; the external-group resolver translates group claims to CF team IDs (raw group IDs never reachtoken_teams); the configured revocation claim (jtidefault,utisupported) is checked against the revocation store on every request. The local user lookup,is_active, the UUID seam, and DB team resolution are skipped by design. The accepted posture change (no per-useris_activekill-switch in trust mode) is documented in code andconfiguration.md. Overage-marked tokens dispatch onjwt_trust_overage_policy.request.statemirrors the default-mode shape (token_teams, derivedteam_id,token_use="trusted",jti,auth_method).Auth-cache Redis keys gain the mode segment (
mcpgw:auth:{version}:{mode}:{key_type}:{identifier}) — a mode flip cold-starts all auth caches with no manual bump; the A.6 prefix test and doctest updated per plan.All strict-xfail suites from #5896 (branch a), #5976, and #6272 flip green in this PR; exactly one xfail remains — the external-IdP branch (b) row pointing at #5903.
Tested with:
uv run pytest tests -k "trust" -q— green; the only XFAIL is the External IdP trust root - claims-derived identity without provisioning #5903 row (grep accounting in the work order report)uv run pytest tests/unit/mcpgateway/test_auth_trust_mode.py -q— 9 passed, including the SQLAlchemy event-listener proof of ZEROemail_usersreads on the trust pathmake test— 23321 passed, 879 skipped, 3 xfailed (the External IdP trust root - claims-derived identity without provisioning #5903 row + 2 pre-existing plugin rows)make ruff— all checks passedmake test-mcp-rbac: implemented astests/live_gateway/test_trust_mode_rbac.py(e2e+skip_no_gateway, verified skipping correctly — no live stack in this worktree); ON/OFF RBAC verified via the unit selection and the flipped matrix rows. The Epic 2 gate (Docs sweep + matrix error-path tests + Epic 2 validation gate #5906) runs the live stack in both modes.Acceptance criteria of #5900 are met. Risk to existing users: none with trust mode off (default) — the branch is unreachable; default-mode paths byte-identical.
Stack: B.8 of epic #5885 (base: #6745).
Closes #5900