Repository navigation
Releases: AuthPlane/python-sdk
Release list
v0.5.0
Released commit: b082846d71d5aaa4c01d58c354e454e81c680b3d
Versioning: This entry contains breaking changes. The project is pre-1.0 (
0.x); per SemVer, breaking changes on the0.xline ship in the next minor (targeting0.5.0), not a major bump.RELEASE_POLICY.md's "major bump for breaking changes" rule takes effect once the project reaches1.0.0.
Added
AuthplaneClient.resource(...),authplane_auth()andauthplane_mcp_auth()acceptresource_metadata_url=, the RFC 9728 §5.1 URL to advertise when the PRM document is AS-hosted; read it back withAuthplaneResource.resource_metadata_url(), which falls back toprm_url().AuthplaneClient.resource(...)logs at INFO when arevocation_checkeris configured with the fail-open default, so the posture shows in startup output.read_dpop_header_from_scope,raw_request_path_from_scopeandget_or_create_verify_cache_from_scopeare exported from the package root: raw-ASGI counterparts of the adapter helpers, for middleware that cannot run under Starlette'sBaseHTTPMiddlewarewith long-livedtext/event-streambodies.validate_prm_resource_identifier,validate_issuer_identifierandvalidate_resource_metadata_urlare exported from the package root, so configuration can be checked before construction without importingauthplane.internal.www_authenticate_challenges(error, *, schemes=None, algs=(), ...)returns oneWWW-Authenticatevalue per scheme, so a resource can advertise bothBearerandDPoP(RFC 9449 §7.1) withalgs.AccessDeniedError(403) andInvalidTargetError(400, RFC 8707 §2.2) for the token-exchange answers authserver 0.2.0 gives; neither counts toward the circuit breaker.AuthplaneClient.resource(...)warns whenIntrospectionRevocationis configured on a client created withoutauth=: authserver ≥ 0.1.2 answersactive: falseto unauthenticated introspection, so every token would be rejected.AuthplaneResourcewarns once per resource when introspection answersactive: falsefor a locally valid token, naming the runtime-client requirement.
Deprecated
VerifiedClaims.may_act— now emitsDeprecationWarning; authserver 0.2.0 no longer issuesmay_act; removed in the next minor.
Security
www_authenticate()no longer copies the exception message intoerror_description; it sends a fixed sentence per RFC 6750 error code. Passverbose_description=Truetowww_authenticate()/response_headers_for()to restore the message in development.
Fixed
- AS metadata is re-read on the verify path, so
metadata_refresh_secondsnow applies to verify-only deployments and a rotatedjwks_uriis followed. The re-read happens after header checks, so malformed tokens trigger no network I/O. jwks_uriis resolved from metadata on every key-set fetch, so a rotation takes effect on the next fetch even without a newkid. Impact: keys left out of the new location stop verifying once the rotation is observed.- A failed JWKS or metadata refresh backs off for
max(1, min(30, refresh_seconds))seconds instead of retrying on the next read. Impact: a blip at a newly rotatedjwks_urican reject the new key for up to that window. - A fetched AS metadata document is validated before it is cached; one that fails the issuer check or names a bad endpoint is discarded and the previous one keeps serving.
InvalidResourceError/InvalidIssuerErrormessages echo only the components the input actually has (still userinfo-redacted), instead of a fixed template that invented the missing parts.authplane-fastmcp,authplane-mcp:authplane_auth(...)/authplane_mcp_auth(...)close the client they created when configuration fails before returning.authplane-fastmcp,authplane-mcp: the path used to build the DPoPhtudrops the query, matching RFC 9449 §4.2. No verification behaviour changes.
Changed
- Docs: introspection now leads with the fail-open consequence and the confidential / runtime-client requirement; token exchange explains
access_deniedvsconsent_required,invalid_targetand the exchange allowlist; the README states authserver compatibility. scripts/manual-e2e-setup.shno longer setsAUTHPLANE_CLIENT_CREDENTIALS_ENABLEDand acceptsAUTHSERVER_REF;scripts/manual-e2e-smoke.shno longer callsPOST /admin/scopes, which authserver 0.2.0 does not serve.- BREAKING (pre-1.0)
ASCredentialsraisesValueErrorwhenclient_idorclient_secretis empty. Migration: pass the real secret, or omitASCredentials. - BREAKING (pre-1.0) A resource identifier must be an absolute URL with a scheme and host, with no userinfo, whitespace or control characters,
"or\in the host, or malformed port; anything else raisesInvalidResourceErrorat construction. Migration: configure the full URL clients use, e.g.https://api.example.com/mcp. - BREAKING (pre-1.0) An issuer must be an absolute URL with a scheme and host, with no userinfo, whitespace or control characters;
AuthplaneClient.create(...),build_metadata_url(/sitelet?url=https%3A%2F%2Fgithub.com%2FAuthPlane%2Fpython-sdk%2F...)andbuild_prm(...)raiseInvalidIssuerErrorotherwise. Migration: fix the configured issuer. - BREAKING (pre-1.0)
build_prm(...)applies the same resource-identifier gate asAuthplaneResource. Migration: pass the identifier you configure onAuthplaneClient.resource(...). - Construction-gate messages say
resource identifierinstead ofresource indicator. Code matching on the old text must be updated. - BREAKING (pre-1.0) The
on_changeargument ofDocumentCache/MetadataCacheand theDocumentChangeCallbackalias are removed fromauthplane.internal. Migration: none expected; passingon_change=is now aTypeError.
v0.4.0
Released commit: eb2fdfa668d9ac7802743fb4ae6582745c537eb3
Versioning: This entry contains breaking changes. The project is pre-1.0 (
0.x); per SemVer, breaking changes on the0.xline ship in the next minor (targeting0.4.0), not a major bump.RELEASE_POLICY.md's "major bump for breaking changes" rule takes effect once the project reaches1.0.0.
Added
authplane-fastmcp,authplane-mcp:authplane_auth()andauthplane_mcp_auth()acceptfail_closed: bool = Falseand forward it toAuthplaneClient.resource(...).AuthplaneClient.resource(...)logs a warning whenfail_closed=Trueis set without arevocation_checker.InvalidIssuerErrorandInvalidResourceError— raised when an issuer carries a query or fragment component (RFC 8414 §2), or a resource indicator carries a fragment (RFC 8707 §2). Both subclassAuthplaneErrorandValueError, so existingexcept ValueErrorhandlers are unaffected; what they add is the ability to tell an identifier misconfiguration apart from any otherValueErrorthe SDK raises. Both identifiers are typed, not just the issuer — the resource rejection below is a behaviour change deployments have to react to, and catching it should not require the undiscriminating handler.authplane-fastmcp:VerbatimPRMRemoteAuthProvideris now public (was_VerbatimPRMRemoteAuthProvider), andrewrite_prm_routes_verbatimis exported as a supported hook. Building aRemoteAuthProviderby hand is a documented FastMCP pattern, and doing so previously lost the verbatim PRM silently.authplane-mcp:AuthplaneTokenVerifier.verbatim_identifiers()returns the configured(issuer, resource)pair, orNonewhen the verifier was not built throughauthplane_mcp_auth.
Security
verify_dpop_proofno longer includes the expected or the received nonce in theInvalidDPoPProofErrorit raises when a configuredexpected_noncepolicy is violated. The message wasDPoP proof nonce mismatch: expected 'server-nonce-abc', got 'stale'; it is nowDPoP proof nonce mismatch.www_authenticatecopiesstr(error)into the challenge'serror_descriptionverbatim, so onceexpected_noncebecomes reachable fromAuthplaneResource.verify()the 401 would hand an unauthenticated caller a currently-valid resource-server nonce — the freshness guarantee the nonce exists to provide (RFC 9449 §9), obtained without the challenge round trip that is supposed to be the only way to get one. Not reachable from the resource path today, which is why it is corrected before the wiring lands rather than after. Migration: code matching on the operand text of this message must match onnonce mismatchinstead._sanitize_header_valuewas never a defence here — it prevents header injection, not disclosure.normalize_dpop_htuno longer collapses an RFC 3986;paramssegment, which weakened DPoP endpoint binding.urlparsepeels the segment off the last path segment into its own slot andurlunparsedropped it, sohttps://api.example.com/mcp;v=1andhttps://api.example.com/mcpnormalized to onehtu. Inbound verification normalizes both the request URL and the proof'shtuthrough this function before comparing them, so those two distinct endpoints compared equal and a proof minted for one was accepted at the other — an acceptance widening on the cross-endpoint replay protection that RFC 9449 §4.3's URI binding exists to provide. RFC 3986 §3.3 places;paramsin the path, and §4.3 removes only query and fragment fromhtu, so the segment has to survive. Now usesurlsplit/urlunsplit, matchinginternal/urls.py; query and fragment are still stripped. Impact: deployments serving endpoints that differ only by a;paramssegment. Outbound proofs now carry the segment inhtu, so a server comparinghtuagainst a request URI containing one will match where it previously did not.- The SSRF-safe fetch path no longer drops an RFC 3986
;paramssegment from the request target.validate_urlparsed withurlparseand the pinned request URL is rebuilt fromValidatedURL.path, so withssrf_protection=True— the default — a call tohttps://as.example.com/token;v=1was issued to/token. Three consequences, all fixed together with thenormalize_dpop_htuchange above, which is what makes the two sides agree: the request silently addressed a different endpoint than the caller named; the outbound DPoP proof'shtu(built from the caller's URL) disagreed with the request line, which an AS rejects as anhtumismatch; and the;params-preserving.well-knownderivation inbuild_prm_url/build_metadata_urlwas undone before it reached the wire. Now usesurlsplit. A malformed port (https://h:abc/) also raisesSSRFErrorrather than a bareValueError—SplitResult.portis parsed lazily, so it escaped the function's own guard. normalize_dpop_htuand the DPoP nonce origin key raiseInvalidDPoPProofErrorinstead of a bareValueErroron a malformed authority (non-numeric or out-of-range port, unterminated IPv6 literal). Inbound verification passes the proof's ownhtuclaim through the normalizer, and the MCP adapters catch onlyAuthplaneError— so a client could turn a 401 into an unhandled 500 with a crafted proof.- The SSRF-safe fetch path no longer downgrades an uppercase scheme to cleartext.
urlsplitnormalizes the scheme, soHTTPS://as.example.com/jwkspassed the HTTPS-only gate invalidate_url— which compares the normalized value — and the pinned request URL was then rebuilt withurl.startswith("https://"), which is false, and issued ashttp://. The port stayed at the validated 443, so the request fails rather than silently downgrading, but the bytes left unencrypted, and on theform_postpath they carry the client's auth headers. Reachable from AS metadata content:jwks_uri,token_endpointandintrospection_endpointare validated against the already-lowercased scheme ininternal/metadata.pyand reachssrf_safe_postverbatim.ValidatedURLnow carries the parsed scheme and the request is built from it, so the value the gate checked is the value that goes on the wire. normalize_dpop_htuand the DPoP nonce origin key no longer emit an unbracketed IPv6 authority.SplitResult.hostnamestrips the brackets RFC 3986 §3.2.2 requires, so reassembling the authority produced an invalid URI —https://[::1]:8080/mcpcame back ashttps://::1:8080/mcp, which this SDK's ownhtuparser then refuses. A proof minted by this SDK against an IPv6 endpoint was therefore rejected by this SDK with both sides honest, reachable in dev mode (allow_localhost) againsthttp://[::1]:port. The nonce key had the same defect, where an unbracketed literal also lets distinct origins collide.net/ssrf.pyalready re-bracketed when reconstructing theHostheader for the same comparison; both halves of that binding check now agree. This was also cross-SDK drift rather than a quirk — go re-brackets explicitly, java'sURI.getHost()and ts's WHATWGURL.hostnamekeep the brackets.internal/metadata.py's endpoint validation raisesMetadataFetchErrorinstead of a bareValueErroron a malformed authority. This is the same untyped-ValueErrorescape closed indpop.py, at the oneurlparsecall site left out of that audit — and this value is AS metadata, i.e. remote content, so an unwrappedValueErrorturned a metadata rejection into an unhandled 500 in the MCP adapters, which catch onlyAuthplaneError.authplane-mcp: themcpdependency floor is now>=1.28.1(was>=1.23.0), pulling in the fix for PYSEC-2026-3483, which affectsmcp <=1.28.0.authplane-fastmcp: now declares a directmcp>=1.28.1,<2dependency. The adapter imports the top-levelmcppackage directly (e.g.mcp.shared.exceptions,mcp.types), so the PYSEC-2026-3483 floor must be pinned here explicitly — the transitivefastmcp>=3.2,<4dependency does not guarantee it.
Fixed
build_prm_urlandbuild_metadata_urlno longer strip slashes from the front of the identifier's path.str.strip("/")removed them from both ends, so a doubled leading slash (//mcp) lost a segment and derived the same well-known URL as/mcp— two distinct identifiers collapsing onto one document. RFC 9728 §3.1 and RFC 8414 §3.1 speak only of the terminating slash.authplane-mcp:install_request_contextno longer accessesmcp.sse_appunguarded. SSE is not on the streamable-HTTP path, so a futuremcp1.x that drops the attribute would have taken down servers that never touch SSE.authplane-fastmcp,authplane-mcp: the elicitation-id resolver raisesRuntimeErrorrather thanImportErrorwhen called lazily — at that point the package imported fine and the failure is a runtime schema mismatch. The import-time call site translates it back toImportError, which is the right shape there.- Docs: the core user guide's revocation section had lost the note that
fail_closedis a no-op without arevocation_checker, and that the SDK warns at resource construction when both are set that way. The behaviour never went away. authplane-mcp:install_request_context(mcp)now detects anAuthplaneTokenVerifierthat carries no verbatim identifiers and warns, instead of silently skipping the PRM rewrite. A verifier built through the publicAuthplaneTokenVerifier(verifier)constructor has no verbatim issuer/resource, so the previous_token_verifier is Nonecheck never fired for it and the served document kept advertising the slash-normalized identifiers this SDK's own byte-for-byte comparison rejects — the exact regression the warning was written to prevent, on the path where it did not fire. Conversely, aFastMCPwith no auth configured no longer produces a spurious "the MCP SDK renamed the private attribute" warning; that wording is now reser...
v0.3.0
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".
v0.2.0
Released commit: 26c28ed5eec94ac83f853a69ecca137f65d50a0e
Security
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.
Fixed
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).
Added
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.
Changed
- CI and release workflow improvements from first-release learnings.