All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
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).
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.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.
- 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".
www_authenticate()now sanitizes CR, LF, double-quote, and backslash from every value it interpolates (realm,error_description,scope,resource_metadata), closing a header-injection path through attacker-influenced error messages.
DPoPNotSupportedErrornow emitsWWW-Authenticate: Bearerinstead ofDPoP. The resource is bearer-only by configuration, so advertising the DPoP scheme misled clients into retries that would fail the same way.http_status(CircuitOpenError)now returns503(was500). The circuit breaker is structurally identical to other temporary-AS-unavailability errors and should be retryable, not surfaced as an internal error.- Outbound
Hostheader now preserves non-default ports and brackets IPv6 hostnames, fixing DPoPhtuvalidation against authservers on non-standard ports. - Packaging issues discovered after the first release.
- Documentation links and demo references.
authplane-fastmcpdependency range now correctly requiresfastmcp>=3.2,<4(was>=2.0, which could resolve to a version the adapter can't import).
www_authenticate()acceptsresource_metadata_url=(RFC 9728 §5.1) andscope=(RFC 6750 §3) keyword arguments. When the caller does not passscope=, the helper auto-populates it fromInsufficientScopeError.required_scopes.InsufficientScopeErrornow carries a structuredrequired_scopesattribute, populated automatically byVerifiedClaims.require_scope()so the wire challenge can advertise the missing scope.response_headers_for(error, *, realm, resource_metadata_url, scope)— bundled helper returning(status, {"WWW-Authenticate": challenge})in one call.- Both adapter verifiers (
authplane-mcp,authplane-fastmcp) now emit alogging.DEBUGeventauthplane.token_verification_failedwith structurederror_classanderrorfields before returningNone. Wire behaviour is unchanged; operators can now distinguish expired tokens from JWKS outages and DPoP replays in logs.
- CI and release workflow improvements from first-release learnings.
- Initial release.