Repository navigation
Conversation
jonpspri
requested review from
Lang-Akshay,
brian-hussey,
crivetimihai,
ja8zyjits and
msureshkumar88
as code owners
September 10, 2026 07:46
jonpspri
added this pull request to stack #6729
September 10, 2026 07:50
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
September 10, 2026 08:06
b4dc120 to
054ffd8
Compare
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
September 10, 2026 14:20
054ffd8 to
6c8f36b
Compare
jonpspri
removed this pull request from stack #6729
September 12, 2026 08:50
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
September 12, 2026 09:07
6c8f36b to
ff2fc67
Compare
jonpspri
added this pull request to stack #6798
September 12, 2026 09:08
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
September 12, 2026 09:20
ff2fc67 to
5ee3ba0
Compare
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
September 12, 2026 09:48
5ee3ba0 to
a880b4b
Compare
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
2 times, most recently
from
September 12, 2026 16:45
590012e to
899809c
Compare
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
September 12, 2026 17:20
899809c to
3a6e02d
Compare
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
September 12, 2026 17:35
3a6e02d to
44f883b
Compare
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
September 12, 2026 17:54
44f883b to
9a59c6d
Compare
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
3 times, most recently
from
September 12, 2026 21:54
e2f1c59 to
d06f96c
Compare
This was referenced Sep 13, 2026
Collaborator
Author
|
Requirement note (remediation) — validator wired The group-existence validator is real now ( Also on this PR: a lazy import broke an import cycle that the remediation set introduced ( |
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
September 21, 2026 09:20
aae0b97 to
88570b3
Compare
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
2 times, most recently
from
September 22, 2026 09:51
44af82f to
d47e6f7
Compare
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
September 22, 2026 09:55
d47e6f7 to
537bbdc
Compare
2 tasks done
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
October 3, 2026 10:06
cae9547 to
a20a644
Compare
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
October 3, 2026 16:31
a20a644 to
853bea1
Compare
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
October 5, 2026 10:03
853bea1 to
31c0ede
Compare
…app-only trust tokens Signed-off-by: Jonathan Springer <jps@s390x.com>
…group mappings Replace the disabled group_exists_validator stub (#5976) with the real Microsoft Graph validator (#5977): Entra issuers resolve the SSO provider record for the issuer (same issuer->provider resolution as the trust-mode overage path) and GET /v1.0/groups/<id> with an app-only token. Graph 200 -> valid, 404 -> graph_group_not_found (recorded, not rejected; the resolver fails closed at read time), any other failure -> unknown under the existing warn-and-allow contract. Non-Entra issuers and issuers without app-only credentials keep the disabled-stub posture (valid) with a log line. The Entra hosts set is single-sourced in entra_graph_client and aliased by OAuthManager._ENTRA_HOSTS. The validator seam stays module-level injectable; sync callables remain supported. Signed-off-by: Jonathan Springer <jps@s390x.com>
…ycle; refresh identity-domains trust row Signed-off-by: Jonathan Springer <jps@s390x.com>
Fold the two post-rebase baseline regenerations into one commit. The plan-document audit entries they carried no longer apply: the stack removed every docs/plans file. Signed-off-by: Jonathan Springer <jps@s390x.com>
…IdP trust funnel
An Entra app-only token (idtyp=app) carries no groups claim and no
overage markers. The bearer funnel already resolved the service
principal's groups under jwt_trust_overage_policy=graph_lookup; the
external-IdP funnel did not, so the same token shape gave different team
visibility on the two surfaces.
build_trusted_external_identity now mirrors the bearer funnel: under
graph_lookup it resolves groups through /servicePrincipals/{oid}/
getMemberObjects with the provider's client-credentials token, feeds the
resolved IDs to the same external-group resolver, and denies with reason
service_principal_groups_unresolved when Graph fails (fail-closed).
fail_closed (default) and proceed_without_groups keep the previous
behavior.
Signed-off-by: Jonathan Springer <jps@s390x.com>
With JWT_TRUST_MODE=jwt-trust, REST accepts a token from a trust root. The streamable-HTTP MCP endpoint rejects the same token with 401. MCP clients use that endpoint, so they cannot authenticate in trust mode. The cause is that _StreamableHttpAuthHandler._auth_jwt never runs the trusted-issuer branch. REST reaches that branch through auth.get_current_user and _try_external_verification. The transport sends the token to the per-server OAuth lookup or to the internal-JWT verifier. Both reject a trust-root token. Add _StreamableHttpAuthHandler._auth_trusted_issuer. _auth_jwt calls it first when trust mode is on. It applies the REST contract: - Call _maybe_verify_external with fail_closed=True. - Reject a failed trust-root token with 401. Log the security event trust_root_token_rejected_ingress. - Check the revocation claim against the revocation store on each request. Reject a revoked token or a missing claim with 401. Return 503 on a database error. - Set the user context and the trace context from the claims. A token from an issuer that is not a trust root continues to the existing paths. AuthContextMiddleware also rejects revoked tokens, but only when security logging, SIEM, the admin API, or password-change enforcement is on. The handler check keeps revocation in force when all four are off. _check_streamable_permission now sends the claims-derived roles and admin flag to RBAC for trust principals only. Correct auth-token-dispatch.md: get_current_user() is the choke point for REST routes only. Describe the trusted-issuer step on the MCP transport. Add unit tests for the handler and the RBAC helper. Add MCP-endpoint rows to the live trust-mode ingress test. Signed-off-by: DJ Lynch <daniel.lynch2016@gmail.com>
Review of the trust-mode transport fix found five problems. RBAC gave the claims-derived admin bypass to any context with token_use="trusted". The internal-JWT path copies that claim from the token, so a gateway-signed token with token_use="trusted" and is_admin skipped the DB admin check. _check_streamable_permission now requires the trust_principal marker, which only _auth_trusted_issuer sets, and trust mode ON. The internal-JWT path also returns 401 for token_use="trusted" when trust mode is OFF, as REST does. The trust branch ran before the per-server OAuth lookup. An oauth_enabled server whose IdP is also a trust root got a 401 for a token minted for its own audience. The trust branch now runs only when the server's OAuth path does not handle the token. The transport accepted any payload from _maybe_verify_external. Its identity cache can return a payload that is not trusted. The transport now requires token_use="trusted" and returns 401 otherwise. An unexpected error in the trust branch returned 500. It now returns 401, as the internal-JWT path does. The transport built the context from the cached payload. REST runs extract_trusted_principal on every request. Move the REST trust steps into two helpers in auth.py: - _is_trust_eligible: the token_use="trusted" dispatch rule, including the 401 when trust mode is OFF. - _resolve_trusted_principal: group-overage resolution, extract_trusted_principal, the email fallback, and the revocation check. get_current_user calls both, with no change in behavior. _auth_trusted_issuer calls _try_external_verification and both helpers. It maps an HTTPException to the same status, detail and headers. Also put team_name in the trust context and forward roles across the internal MCP seam. Signed-off-by: DJ Lynch <daniel.lynch2016@gmail.com>
The choke point calls verify_credentials_cached since main's shared verifier landed (#6396). Rename the patch target so the double controls the funnel under test. Signed-off-by: Jonathan Springer <jps@s390x.com>
jonpspri
force-pushed
the
feat/6756-app-only-graph-lookup
branch
from
October 5, 2026 12:39
31c0ede to
6328d99
Compare
This branch has not been deployed
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.
Adds app-only (client-credentials) trust-token group resolution to JWT-trust mode (issue #6756, epic #5885).
App-only Entra tokens carry
idtyp="app", arolesclaim, and nogroupsclaim. Entra emits no overage markers for them, so the #5977 overage dispatch never triggers — and/users/{oid}/getMemberObjectswould fail for a service principal (a service principal is not a user). Underjwt_trust_overage_policy="graph_lookup"this PR resolves the service principal's security groups throughPOST /servicePrincipals/{oid}/getMemberObjectsand maps them throughexternal_group_mappings.Changes:
detect_app_only_token(payload): True whenidtyp == "app"(mcpgateway/utils/trusted_claims.py).EntraGraphClientendpoint selection:/servicePrincipals/{oid}/getMemberObjectsfor app-only tokens;/users/{oid}/getMemberObjectsunchanged for user tokens. Client-credentials grant only; the inbound bearer token is never used. Same oid-keyed Redis cache, TTL bounded byexp.get_current_user):idtyp == "app"ANDgroupsabsent ANDgraph_lookup-> Graph resolve (cached) -> payload copy ->resolve_external_groups_to_teams. Underfail_closed(default) andproceed_without_groupsan app token without groups authenticates withtoken_teams=[]; the app-role path (Admin claim feeds both admin tracks atomically + parity tests #5902) stays intact.make_trusted_test_jwt: newidtypkwarg; default output unchanged.TDD: 7 new tests in
tests/unit/mcpgateway/test_entra_graph_client.py(red first: ImportError on the new symbols). URL capture asserts/servicePrincipals/{id}is called and/users/never is for app tokens;fail_closedpins aroles=["viewer"]app token (authenticated,token_teams=[], viewer role granted); Graph failure undergraph_lookup-> 401; Redis read error -> cache miss -> live call. All 11 pre-existing tests in the file pass unmodified (user-token overage regression).Gate:
make ruff— All checks passed!make test— 23411 passed, 879 skipped, 2 xfailed. (Two earlier full-suite runs each flaked on the pre-existing wall-clock benchmarktest_trust_p99_within_2x_defaultunder load; it passes in isolation and in the final green run. This PR adds no timing tests.)Note: the commit also carries the pre-staged
.secrets.baselineregeneration (line-number bookkeeping for existingis_secret: falseentries), which was already in the index from the stack work.Risk to existing users: none — every new branch is trust-mode + graph_lookup gated; default mode and the user-token overage path verified green in the full suite.
Stack: B.15 of epic #5885 (base: #6755).
Closes #6756
External-IdP funnel parity (folded in)
build_trusted_external_identitynow resolves app-only tokens' service-principal groups underjwt_trust_overage_policy=graph_lookup, mirroringget_current_user: when a token carriesidtyp == "app", nogroupsclaim, and no overage marker, the funnel queries/servicePrincipals/{oid}/getMemberObjectswith the provider's client-credentials token (the inbound bearer is never used) and feeds the resolved IDs to the same external-group resolver. A Graph failure denies with metric reasonservice_principal_groups_unresolved(fail-closed).fail_closedandproceed_without_groupskeep the previous behavior. Four deny-path/positive unit tests cover the dispatch intest_external_idp_trust_mode.py; use case 5 of the live Entra suite (#6931) proves it against a real tenant.