Sitelet https://github.com/AuthPlane/python-sdk/releases
Skip to content

Releases: AuthPlane/python-sdk

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 01 Oct 16:17

Released commit: b082846d71d5aaa4c01d58c354e454e81c680b3d

Versioning: This entry contains breaking changes. The project is pre-1.0 (0.x); per SemVer, breaking changes on the 0.x line ship in the next minor (targeting 0.5.0), not a major bump. RELEASE_POLICY.md's "major bump for breaking changes" rule takes effect once the project reaches 1.0.0.

Added

  • AuthplaneClient.resource(...), authplane_auth() and authplane_mcp_auth() accept resource_metadata_url=, the RFC 9728 §5.1 URL to advertise when the PRM document is AS-hosted; read it back with AuthplaneResource.resource_metadata_url(), which falls back to prm_url().
  • AuthplaneClient.resource(...) logs at INFO when a revocation_checker is configured with the fail-open default, so the posture shows in startup output.
  • read_dpop_header_from_scope, raw_request_path_from_scope and get_or_create_verify_cache_from_scope are exported from the package root: raw-ASGI counterparts of the adapter helpers, for middleware that cannot run under Starlette's BaseHTTPMiddleware with long-lived text/event-stream bodies.
  • validate_prm_resource_identifier, validate_issuer_identifier and validate_resource_metadata_url are exported from the package root, so configuration can be checked before construction without importing authplane.internal.
  • www_authenticate_challenges(error, *, schemes=None, algs=(), ...) returns one WWW-Authenticate value per scheme, so a resource can advertise both Bearer and DPoP (RFC 9449 §7.1) with algs.
  • AccessDeniedError (403) and InvalidTargetError (400, RFC 8707 §2.2) for the token-exchange answers authserver 0.2.0 gives; neither counts toward the circuit breaker.
  • AuthplaneClient.resource(...) warns when IntrospectionRevocation is configured on a client created without auth=: authserver ≥ 0.1.2 answers active: false to unauthenticated introspection, so every token would be rejected.
  • AuthplaneResource warns once per resource when introspection answers active: false for a locally valid token, naming the runtime-client requirement.

Deprecated

  • VerifiedClaims.may_act — now emits DeprecationWarning; authserver 0.2.0 no longer issues may_act; removed in the next minor.

Security

  • www_authenticate() no longer copies the exception message into error_description; it sends a fixed sentence per RFC 6750 error code. Pass verbose_description=True to www_authenticate() / response_headers_for() to restore the message in development.

Fixed

  • AS metadata is re-read on the verify path, so metadata_refresh_seconds now applies to verify-only deployments and a rotated jwks_uri is followed. The re-read happens after header checks, so malformed tokens trigger no network I/O.
  • jwks_uri is resolved from metadata on every key-set fetch, so a rotation takes effect on the next fetch even without a new kid. 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 rotated jwks_uri can 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 / InvalidIssuerError messages 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 DPoP htu drops 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_denied vs consent_required, invalid_target and the exchange allowlist; the README states authserver compatibility.
  • scripts/manual-e2e-setup.sh no longer sets AUTHPLANE_CLIENT_CREDENTIALS_ENABLED and accepts AUTHSERVER_REF; scripts/manual-e2e-smoke.sh no longer calls POST /admin/scopes, which authserver 0.2.0 does not serve.
  • BREAKING (pre-1.0) ASCredentials raises ValueError when client_id or client_secret is empty. Migration: pass the real secret, or omit ASCredentials.
  • 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 raises InvalidResourceError at 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...) and build_prm(...) raise InvalidIssuerError otherwise. Migration: fix the configured issuer.
  • BREAKING (pre-1.0) build_prm(...) applies the same resource-identifier gate as AuthplaneResource. Migration: pass the identifier you configure on AuthplaneClient.resource(...).
  • Construction-gate messages say resource identifier instead of resource indicator. Code matching on the old text must be updated.
  • BREAKING (pre-1.0) The on_change argument of DocumentCache / MetadataCache and the DocumentChangeCallback alias are removed from authplane.internal. Migration: none expected; passing on_change= is now a TypeError.

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 28 Aug 16:30

Released commit: eb2fdfa668d9ac7802743fb4ae6582745c537eb3

Versioning: This entry contains breaking changes. The project is pre-1.0 (0.x); per SemVer, breaking changes on the 0.x line ship in the next minor (targeting 0.4.0), not a major bump. RELEASE_POLICY.md's "major bump for breaking changes" rule takes effect once the project reaches 1.0.0.

Added

  • authplane-fastmcp, authplane-mcp: authplane_auth() and authplane_mcp_auth() accept fail_closed: bool = False and forward it to AuthplaneClient.resource(...).
  • AuthplaneClient.resource(...) logs a warning when fail_closed=True is set without a revocation_checker.
  • InvalidIssuerError and InvalidResourceError — raised when an issuer carries a query or fragment component (RFC 8414 §2), or a resource indicator carries a fragment (RFC 8707 §2). Both subclass AuthplaneError and ValueError, so existing except ValueError handlers are unaffected; what they add is the ability to tell an identifier misconfiguration apart from any other ValueError the 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: VerbatimPRMRemoteAuthProvider is now public (was _VerbatimPRMRemoteAuthProvider), and rewrite_prm_routes_verbatim is exported as a supported hook. Building a RemoteAuthProvider by 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, or None when the verifier was not built through authplane_mcp_auth.

Security

  • verify_dpop_proof no longer includes the expected or the received nonce in the InvalidDPoPProofError it raises when a configured expected_nonce policy is violated. The message was DPoP proof nonce mismatch: expected 'server-nonce-abc', got 'stale'; it is now DPoP proof nonce mismatch. www_authenticate copies str(error) into the challenge's error_description verbatim, so once expected_nonce becomes reachable from AuthplaneResource.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 on nonce mismatch instead. _sanitize_header_value was never a defence here — it prevents header injection, not disclosure.
  • normalize_dpop_htu no longer collapses an RFC 3986 ;params segment, which weakened DPoP endpoint binding. urlparse peels the segment off the last path segment into its own slot and urlunparse dropped it, so https://api.example.com/mcp;v=1 and https://api.example.com/mcp normalized to one htu. Inbound verification normalizes both the request URL and the proof's htu through 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 ;params in the path, and §4.3 removes only query and fragment from htu, so the segment has to survive. Now uses urlsplit/urlunsplit, matching internal/urls.py; query and fragment are still stripped. Impact: deployments serving endpoints that differ only by a ;params segment. Outbound proofs now carry the segment in htu, so a server comparing htu against a request URI containing one will match where it previously did not.
  • The SSRF-safe fetch path no longer drops an RFC 3986 ;params segment from the request target. validate_url parsed with urlparse and the pinned request URL is rebuilt from ValidatedURL.path, so with ssrf_protection=True — the default — a call to https://as.example.com/token;v=1 was issued to /token. Three consequences, all fixed together with the normalize_dpop_htu change above, which is what makes the two sides agree: the request silently addressed a different endpoint than the caller named; the outbound DPoP proof's htu (built from the caller's URL) disagreed with the request line, which an AS rejects as an htu mismatch; and the ;params-preserving .well-known derivation in build_prm_url/build_metadata_url was undone before it reached the wire. Now uses urlsplit. A malformed port (https://h:abc/) also raises SSRFError rather than a bare ValueError — SplitResult.port is parsed lazily, so it escaped the function's own guard.
  • normalize_dpop_htu and the DPoP nonce origin key raise InvalidDPoPProofError instead of a bare ValueError on a malformed authority (non-numeric or out-of-range port, unterminated IPv6 literal). Inbound verification passes the proof's own htu claim through the normalizer, and the MCP adapters catch only AuthplaneError — 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. urlsplit normalizes the scheme, so HTTPS://as.example.com/jwks passed the HTTPS-only gate in validate_url — which compares the normalized value — and the pinned request URL was then rebuilt with url.startswith("https://"), which is false, and issued as http://. The port stayed at the validated 443, so the request fails rather than silently downgrading, but the bytes left unencrypted, and on the form_post path they carry the client's auth headers. Reachable from AS metadata content: jwks_uri, token_endpoint and introspection_endpoint are validated against the already-lowercased scheme in internal/metadata.py and reach ssrf_safe_post verbatim. ValidatedURL now 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_htu and the DPoP nonce origin key no longer emit an unbracketed IPv6 authority. SplitResult.hostname strips the brackets RFC 3986 §3.2.2 requires, so reassembling the authority produced an invalid URI — https://[::1]:8080/mcp came back as https://::1:8080/mcp, which this SDK's own htu parser 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) against http://[::1]:port. The nonce key had the same defect, where an unbracketed literal also lets distinct origins collide. net/ssrf.py already re-bracketed when reconstructing the Host header 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's URI.getHost() and ts's WHATWG URL.hostname keep the brackets.
  • internal/metadata.py's endpoint validation raises MetadataFetchError instead of a bare ValueError on a malformed authority. This is the same untyped-ValueError escape closed in dpop.py, at the one urlparse call site left out of that audit — and this value is AS metadata, i.e. remote content, so an unwrapped ValueError turned a metadata rejection into an unhandled 500 in the MCP adapters, which catch only AuthplaneError.
  • authplane-mcp: the mcp dependency floor is now >=1.28.1 (was >=1.23.0), pulling in the fix for PYSEC-2026-3483, which affects mcp <=1.28.0.
  • authplane-fastmcp: now declares a direct mcp>=1.28.1,<2 dependency. The adapter imports the top-level mcp package directly (e.g. mcp.shared.exceptions, mcp.types), so the PYSEC-2026-3483 floor must be pinned here explicitly — the transitive fastmcp>=3.2,<4 dependency does not guarantee it.

Fixed

  • build_prm_url and build_metadata_url no 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_context no longer accesses mcp.sse_app unguarded. SSE is not on the streamable-HTTP path, so a future mcp 1.x that drops the attribute would have taken down servers that never touch SSE.
  • authplane-fastmcp, authplane-mcp: the elicitation-id resolver raises RuntimeError rather than ImportError when 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 to ImportError, which is the right shape there.
  • Docs: the core user guide's revocation section had lost the note that fail_closed is a no-op without a revocation_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 an AuthplaneTokenVerifier that carries no verbatim identifiers and warns, instead of silently skipping the PRM rewrite. A verifier built through the public AuthplaneTokenVerifier(verifier) constructor has no verbatim issuer/resource, so the previous _token_verifier is None check 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, a FastMCP with no auth configured no longer produces a spurious "the MCP SDK renamed the private attribute" warning; that wording is now reser...
Read more

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 18:28

Released commit: 4f066599cbd9f22f6d2cdbc60a193f0a0cf159cb

Added

  • TokenCache is now bounded by a configurable max_entries cap (default 10_000, exposed as TokenCache.DEFAULT_MAX_ENTRIES and a read-only cache.max_entries property) and evicts the least-recently-used entry on overflow; both get and set bump the touched key to MRU. 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 helper that requires all listed scopes. 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 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 a ContextVar so the verifier can build a DPoPRequestContext.
  • authplane-fastmcp, authplane-mcp: AuthplaneTokenVerifier caches the in-flight verify task per request (keyed by access token on request.state), so a repeat verify_token within 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.set now distinguishes a missing expires_in from expires_in: 0. Both previously collapsed into default_ttl. The store now applies default_ttl only when expires_in is absent (None), treats expires_in: 0 (RFC 6749 §5.1) as already expired and refuses to store it, and honors n seconds when positive. parse_token_response carries the missing-vs-zero distinction through the parser.
  • TokenCache now preserves the DPoP key thumbprint (cnf_jkt, RFC 9449 §6.1) across a cache round-trip: CacheEntry persists cnf_jkt and client_credentials propagates 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_header reads the full multi-value DPoP header list (and splits on , defensively to catch proxies that pre-join duplicate headers) and raises DPoPMultipleProofsError when more than one non-empty proof is present. www_authenticate maps this error to error="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_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. Operators using required=True with authplane-mcp should call install_request_context(mcp) after constructing FastMCP so 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: DPoP 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.
  • authplane-mcp: install_request_context(mcp) is idempotent — repeated calls on the same FastMCP instance 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 old Token has scopes: [] string should be updated.
  • Docs and demos 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.

Changed

  • BREAKING (pre-1.0) TokenResponse.expires_in and CacheEntry.expires_in are now typed int | None (was int), so a token response that omits expires_in is None rather than 0. Migration: typed downstream callers reading resp.expires_in directly (arithmetic, comparison, formatting) must guard for None; treat None as "apply your default" and 0 as "already expired".

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 20 May 13:13

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

  • DPoPNotSupportedError now emits WWW-Authenticate: Bearer instead of DPoP. 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 returns 503 (was 500). The circuit breaker is structurally identical to other temporary-AS-unavailability errors and should be retryable, not surfaced as an internal error.
  • Outbound Host header now preserves non-default ports and brackets IPv6 hostnames, fixing DPoP htu validation against authservers on non-standard ports.
  • Packaging issues discovered after the first release.
  • Documentation links and demo references.
  • authplane-fastmcp dependency range now correctly requires fastmcp>=3.2,<4 (was >=2.0, which could resolve to a version the adapter can't import).

Added

  • www_authenticate() accepts resource_metadata_url= (RFC 9728 §5.1) and scope= (RFC 6750 §3) keyword arguments. When the caller does not pass scope=, the helper auto-populates it from InsufficientScopeError.required_scopes.
  • InsufficientScopeError now carries a structured required_scopes attribute, populated automatically by VerifiedClaims.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 a logging.DEBUG event authplane.token_verification_failed with structured error_class and error fields before returning None. 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.

v0.1.0

Choose a tag to compare

@muralx muralx released this 13 May 10:24

Released commit: 34f030a

  • Initial release.