feat: enforce inbound DPoP in MCP adapters, bound TokenCache, add require_scopes#12
Merged
muralx merged 1 commit intoJun 16, 2026
Conversation
…uire_scopes Adapters (authplane-mcp, authplane-fastmcp): - Forward a DPoPRequestContext from verify_token to AuthplaneResource.verify so inbound_dpop=InboundDPoPOptions(required=True) enforces the proof check end-to-end. htu origin is always the operator-configured resource URI, never the inbound Host / X-Forwarded-Proto headers. - Reconstruct htu from scope["raw_path"] to preserve percent-encoding (e.g. %2F) on the wire under ASGI, falling back to request.url.path. - authplane-mcp adds AuthplaneRequestContextMiddleware, get_current_request(), and install_request_context(mcp) (idempotent) to publish the active request on a ContextVar so the verifier can build a DPoPRequestContext. - Cache the in-flight verify task per request on request.state so a repeat verify_token within the same request reuses it instead of re-entering the inbound DPoP replay store. Cross-request replay protection is unaffected. Core (authplane): - Bound TokenCache with a configurable max_entries cap (default 10_000, exposed as TokenCache.DEFAULT_MAX_ENTRIES) and LRU eviction; plumbed through AuthplaneClient.create(cache_max_entries=...). - Add VerifiedClaims.require_scopes(scopes) plural AND-style scope helper. Empty input is a no-op; raises InsufficientScopeError listing all missing scopes and the token's available scopes on failure. - Export TokenCache from authplane.__init__. - require_scope (singular) now renders an empty scope set as (none) instead of [], consistent with the plural helper. Docs and demos run adapter setup, the async server entry point, and aclose() in a single asyncio.run(main()), keeping the client's locks, HTTP pool, and background refresh tasks on one event loop.
RobertoIskandarani
approved these changes
Jun 16, 2026
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
Three improvements to the adapters and core, batched into one release:
TokenCacheis bounded with a configurable cap + LRU eviction.VerifiedClaims.require_scopes(...)— a new plural AND-style scope helper.Plus a docs/demo update for the adapter event-loop lifecycle.
Adapters (
authplane-mcp,authplane-fastmcp)DPoP enforcement
AuthplaneTokenVerifier.verify_tokenforwards aDPoPRequestContext(method + reconstructedhtu+ proof header) toAuthplaneResource.verify, soinbound_dpop=InboundDPoPOptions(required=True)checks the proof on every request. Thehtuorigin is always the operator-configured resource URI — never the inboundHost/X-Forwarded-Protoheaders.htureconstruction readsscope["raw_path"]to preserve percent-encoding (e.g.%2F) on the wire under ASGI, falling back torequest.url.pathwhen the server omitsraw_path.New public surface in
authplane-mcpAuthplaneRequestContextMiddleware,get_current_request(), andinstall_request_context(mcp)(idempotent) — an ASGI middleware that publishes the active request on aContextVarfor the verifier. The MCP SDK'sTokenVerifierprotocol has no per-request hook, so this bridges the request context to the verifier.AuthplaneTokenVerifiercaches the in-flight verify task per request (keyed by access token onrequest.state), so a repeatverify_tokenwithin one request reuses it instead of re-entering the inbound DPoP replay store. Cross-request replay protection is unaffected.Core (
authplane)TokenCacheis bounded by a configurablemax_entriescap (default10_000, exposed asTokenCache.DEFAULT_MAX_ENTRIESand a read-onlycache.max_entriesproperty) with LRU eviction; plumbed throughAuthplaneClient.create(cache_max_entries=...). Token-exchange cache keys are high-cardinality (the subject token is part of the key), so the cap keeps long-lived clients bounded.VerifiedClaims.require_scopes(scopes: Iterable[str])— plural AND-style helper that requires all listed scopes. Empty input is a no-op; on failure the raisedInsufficientScopeErrorcarries the full requested tuple onrequired_scopesand names every missing scope plus the token's available scopes.TokenCacheis now exported fromauthplane.__init__.require_scope(singular) now renders an empty scope set as(none)instead of[].Docs / demos
run_streamable_http_async/run_async), andaclose()in a singleasyncio.run(main()), keeping the client's locks, HTTP pool, and background JWKS/metadata refresh tasks on one event loop.Testing
ruff check+ruff format --check,pyright(0 errors)authplane-mcp51 ·authplane-fastmcp51 · conformance 104 passed / 1 xfailedpython -m build+twine checkpassed