Released commit: 4f066599cbd9f22f6d2cdbc60a193f0a0cf159cb
Added
TokenCacheis now bounded by a configurablemax_entriescap (default10_000, exposed asTokenCache.DEFAULT_MAX_ENTRIESand a read-onlycache.max_entriesproperty) and evicts the least-recently-used entry on overflow; bothgetandsetbump the touched key to MRU. 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 in the message.authplane-mcp: new public surface —AuthplaneRequestContextMiddleware,get_current_request(),install_request_context(mcp)— an ASGI middleware that publishes the active request on aContextVarso the verifier can build aDPoPRequestContext.authplane-fastmcp,authplane-mcp:AuthplaneTokenVerifiercaches the in-flight verify task per request (keyed by access token onrequest.state), so a repeatverify_tokenwithin the same HTTP request awaits the same task rather than re-entering the inbound DPoP replay store. Cross-request replay protection is unaffected (distinct requests get distinct caches).
Fixed
TokenCache.setnow distinguishes a missingexpires_infromexpires_in: 0. Both previously collapsed intodefault_ttl. The store now appliesdefault_ttlonly whenexpires_inis absent (None), treatsexpires_in: 0(RFC 6749 §5.1) as already expired and refuses to store it, and honorsnseconds when positive.parse_token_responsecarries the missing-vs-zero distinction through the parser.TokenCachenow preserves the DPoP key thumbprint (cnf_jkt, RFC 9449 §6.1) across a cache round-trip:CacheEntrypersistscnf_jktandclient_credentialspropagates it on a cache hit, so a sender-constrained token retrieved from cache reports its binding to downstream callers instead of silently degrading to a bearer-only shape.authplane-fastmcp,authplane-mcp: inbound DPoP cardinality (RFC 9449 §4.3 #1) is now enforced.read_dpop_headerreads the full multi-valueDPoPheader list (and splits on,defensively to catch proxies that pre-join duplicate headers) and raisesDPoPMultipleProofsErrorwhen more than one non-empty proof is present.www_authenticatemaps this error toerror="invalid_dpop_proof"per RFC 9449 §7.1.authplane-fastmcp,authplane-mcp: inbound DPoP proof-of-possession is now enforced end-to-end.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. Operators usingrequired=Truewithauthplane-mcpshould callinstall_request_context(mcp)after constructingFastMCPso the verifier can read the per-request context; if it is not installed the request fails closed (401) rather than skipping the check.authplane-fastmcp,authplane-mcp: DPoPhtureconstruction readsscope["raw_path"]to preserve percent-encoding (e.g.%2F) on the wire under ASGI, falling back torequest.url.pathwhen the server omitsraw_path.authplane-mcp:install_request_context(mcp)is idempotent — repeated calls on the sameFastMCPinstance are no-ops.require_scope(singular) now renders an empty token scope set as(none)instead of[], matching the plural helper's output. Logging pipelines keyed on the oldToken has scopes: []string should be updated.- Docs and demos now run adapter setup, the async server entry point (
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.
Changed
- BREAKING (pre-1.0)
TokenResponse.expires_inandCacheEntry.expires_inare now typedint | None(wasint), so a token response that omitsexpires_inisNonerather than0. Migration: typed downstream callers readingresp.expires_indirectly (arithmetic, comparison, formatting) must guard forNone; treatNoneas "apply your default" and0as "already expired".