Skip to content

feat: enforce inbound DPoP in MCP adapters, bound TokenCache, add require_scopes#11

Closed
muralx wants to merge 1 commit into
mainfrom
feat/adapter-dpop-enforcement-and-cache-bounds
Closed

feat: enforce inbound DPoP in MCP adapters, bound TokenCache, add require_scopes#11
muralx wants to merge 1 commit into
mainfrom
feat/adapter-dpop-enforcement-and-cache-bounds

Conversation

@muralx

@muralx muralx commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Three improvements to the adapters and core, batched into one release:

  1. Inbound DPoP proof-of-possession is now enforced end-to-end in the MCP adapters.
  2. TokenCache is bounded with a configurable cap + LRU eviction.
  3. 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_token forwards a DPoPRequestContext (method + reconstructed htu + proof header) to AuthplaneResource.verify, so inbound_dpop=InboundDPoPOptions(required=True) checks the proof on every request. The htu origin is always the operator-configured resource URI — never the inbound Host / X-Forwarded-Proto headers.
  • htu reconstruction reads scope["raw_path"] to preserve percent-encoding (e.g. %2F) on the wire under ASGI, falling back to request.url.path when the server omits raw_path.

New public surface in authplane-mcp

  • AuthplaneRequestContextMiddleware, get_current_request(), and install_request_context(mcp) (idempotent) — an ASGI middleware that publishes the active request on a ContextVar for the verifier. The MCP SDK's TokenVerifier protocol has no per-request hook, so this bridges the request context to the verifier.
  • AuthplaneTokenVerifier caches the in-flight verify task per request (keyed by access token on request.state), so a repeat verify_token within one request reuses it instead of re-entering the inbound DPoP replay store. Cross-request replay protection is unaffected.

Note for required=True operators: call install_request_context(mcp) after constructing FastMCP so the verifier can read the per-request context. If it isn't installed, requests fail closed (401) rather than skipping the check — safe by default.

Core (authplane)

  • TokenCache is bounded by a configurable max_entries cap (default 10_000, exposed as TokenCache.DEFAULT_MAX_ENTRIES and a read-only cache.max_entries property) with LRU eviction; plumbed through AuthplaneClient.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 scope-union helper. Empty input is a no-op; on failure the raised InsufficientScopeError carries the full requested tuple on required_scopes and names every missing scope plus the token's available scopes.

Docs / demos

  • Examples now run adapter setup, the async server entry point (run_streamable_http_async / run_async), and aclose() in a single asyncio.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)
  • root 503 passed · authplane-mcp 51 · authplane-fastmcp 51 · conformance 104 passed / 1 xfailed
  • python -m build + twine check passed

@muralx muralx requested a review from a team as a code owner June 10, 2026 10:45

@RobertoIskandarani RobertoIskandarani left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

…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.
@muralx muralx force-pushed the feat/adapter-dpop-enforcement-and-cache-bounds branch from 5f0c8ee to 09c92a1 Compare June 15, 2026 11:01
@muralx muralx closed this Jun 15, 2026
@muralx muralx deleted the feat/adapter-dpop-enforcement-and-cache-bounds branch June 15, 2026 11:09
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