Identifier identity and validation, DPoP htu ;params, verbatim PRM hooks, pinned conformance catalog - #25
Open
RobertoIskandarani wants to merge 5 commits into
Open
Identifier identity and validation, DPoP htu ;params, verbatim PRM hooks, pinned conformance catalog#25RobertoIskandarani wants to merge 5 commits into
RobertoIskandarani wants to merge 5 commits into
Conversation
… branch CI cloned github.com/AuthPlane/conformance at its default branch, so any change to the catalog reached this repo immediately — a case added there could turn an unrelated PR red here with nothing in this repo having changed. Pinning decouples them: a catalog change arrives only when this repo deliberately bumps the pin together with the coverage for it. The revision is single-sourced in a tracked .conformance-catalog-ref, guarded by a 40-hex shape check before the fetch so a branch or tag name cannot silently un-pin CI, and read by both ci.yml and release.yml. A weekly conformance-catalog-drift workflow clones the unpinned tip and runs the alignment assertion, so new cases surface as an early warning instead of a surprise at bump time. No SDK source changes. Verified locally against the pinned revision: the catalog alignment assertion passes, the conformance suite is green (104 passed, 1 xfailed — the xfail is pre-existing and unrelated), and the core suite is green (525). The pin adopts no new case ids, so it requires no coverage change.
…aise mcp floor past PYSEC-2026-3483 - authplane-sdk: issuer identifiers are stored and compared byte-for-byte (RFC 9068 iss, RFC 8414 §3.3) — no trailing-slash stripping on storage or comparison; query- or fragment-bearing issuers are rejected at AuthplaneClient.create() (RFC 8414 §2). Well-known derivation still strips the terminating slash (RFC 8414/9728 §3.1); build_prm_url keeps the resource query and rejects fragment-bearing resources (RFC 8707 §2). - authplane-mcp, authplane-fastmcp: the served Protected Resource Metadata advertises the configured issuer and resource byte-for-byte instead of the AnyHttpUrl-normalised form (RFC 8414/9728 §3.3). - authplane-mcp: mcp floor raised to >=1.28.1,<2 (PYSEC-2026-3483); the elicitation-id field name is resolved from the model schema instead of being hard-coded. authplane-fastmcp declares the same direct floor. - ci: single-source the conformance catalog pin in .conformance-catalog-ref (40-hex guarded) and add a scheduled drift check; pin ruff to >=0.16,<0.17 so formatting stays stable.
…h matchable errors
The fragment check for a resource indicator lived only in `build_prm_url`, whose
production caller is `AuthplaneResource.prm_url()` — a method reached from
inside a 401 response path. A deployment configured with a fragment-bearing
identifier therefore started normally and failed later, from the failure path,
as a 500. It is now rejected at `AuthplaneClient.resource(...)`, symmetrically
with the issuer check in `create()`, and again in `AuthplaneResource` itself so
the gate holds for a resource built without the factory. The check in
`build_prm_url` stays as a backstop.
`InvalidIssuerError` and `InvalidResourceError` subclass both `AuthplaneError`
and `ValueError`, so existing `except ValueError` handlers keep working while
callers gain the ability to tell an identifier misconfiguration apart from any
other `ValueError` the SDK raises — which matters now that the resource
rejection is a behaviour change deployments have to react to.
`build_prm_url` and `build_metadata_url` no longer strip slashes from the front
of the path: `str.strip("/")` took them from both ends, so `//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.
BREAKING CHANGE: a fragment-bearing resource identifier now raises
`InvalidResourceError` at `client.resource(...)` instead of starting and failing
later. Remove the fragment — RFC 8707 §2 forbids one in a resource indicator.
…ake the PRM hook public `urlparse` peels an RFC 3986 `;params` segment off the last path segment, so a request to `/orders;v=2` bound an `htu` of `/orders` — a proof computed over a different resource than the one being accessed, and RFC 3986 §3.3 puts `;params` squarely in the path. The htu construction uses `urlsplit`, which does not split it off, and the round-trip no longer has to fill `urlunparse`'s params slot. `VerbatimPRMRemoteAuthProvider` is now public (was `_VerbatimPRMRemoteAuthProvider`) and `rewrite_prm_routes_verbatim` is exported: building a `RemoteAuthProvider` by hand is a documented FastMCP pattern, and doing so silently lost the verbatim PRM. `install_request_context(mcp)` now detects a verifier carrying no verbatim identifiers and warns instead of skipping the rewrite in silence — a verifier built through the public `AuthplaneTokenVerifier(verifier)` constructor has none, so the previous `is None` check never fired for it and the served document kept advertising slash-normalized identifiers that this SDK's own byte-for-byte comparison rejects. `AuthplaneTokenVerifier.verbatim_identifiers()` exposes the pair without cross-module private attribute reads. `install_request_context` also stops touching `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.
…kfiles, record the changelog The conformance suite stops xfailing the inbound DPoP-Nonce case now that the catalog carries it, and the catalog-fetch tooling is aligned with the pinned ref so a local run matches CI rather than the moving default branch. `backport-fixes.sh` accepts a tag as `--from`: `release.yml` deletes `release/vX.Y.Z` once the tag is pushed, so afterwards the tag is the only ref naming those commits — which is exactly what the release summary tells the operator to pass. `--from` and `--to` are validated as ref names before reaching a fetch refspec, since `git ls-remote` matches its arguments as globs. `uv.lock` is ignored: nothing tracks or consumes these files — no workflow installs with uv — so they are local resolution artifacts. Three of them were swept into a merge commit as untracked files once and accounted for 94% of that diff. Tracking them for reproducible installs is a real decision and belongs in its own PR, alongside the CI change that would make them load-bearing.
| with pytest.raises(ValueError) as exc: | ||
| validate_resource_indicator("https://svc:s3cr3t@api.example.com/mcp#frag") | ||
| assert "s3cr3t" not in str(exc.value) | ||
| assert "api.example.com" in str(exc.value) |
| # call site the failure really is "the installed distribution is unusable", so | ||
| # translate it to the shape a reader expects from a failing import. | ||
| try: | ||
| _ELICITATION_ID_KWARG = _resolve_elicitation_id_kwarg(ElicitRequestURLParams) |
| # call site the failure really is "the installed distribution is unusable", so | ||
| # translate it to the shape a reader expects from a failing import. | ||
| try: | ||
| _ELICITATION_ID_KWARG = _resolve_elicitation_id_kwarg(ElicitRequestURLParams) |
| # RemoteAuthProvider does not silently lose the verbatim PRM. Nothing else | ||
| # imports them from the root — conftest reaches into .auth — so without this | ||
| # the __all__ entries could rot without a test noticing. | ||
| import authplane_fastmcp |
| from mcp.types import URL_ELICITATION_REQUIRED, ElicitRequestURLParams | ||
| from pydantic import BaseModel | ||
|
|
||
| import authplane_mcp.url_elicitation as url_elicitation |
| from mcp.types import URL_ELICITATION_REQUIRED, ElicitRequestURLParams | ||
| from pydantic import BaseModel | ||
|
|
||
| import authplane_fastmcp.url_elicitation as url_elicitation |
|
|
||
| def __call__( | ||
| self, base_url: str, resource: str, *, scopes: list[str] | None = None | ||
| ) -> AuthplaneTokenVerifier: ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Everything queued for the next release, consolidated into a single branch so it reviews and lands as one unit. Two behaviour changes are breaking; both carry a migration note in
CHANGELOG.md.Identifier identity and validation
The configured issuer is preserved byte-for-byte and compared for identity (RFC 8414 §2/§3.3); the terminating slash is dropped only where a well-known URL is derived (RFC 8414/9728 §3.1). An issuer carrying a query or fragment is rejected at
AuthplaneClient.create().The matching gate for the resource indicator previously lived only in
build_prm_url, whose production caller isAuthplaneResource.prm_url()— a method reached from inside a 401 response path. A deployment configured with a fragment-bearing identifier therefore started normally and surfaced the problem later as a 500 emitted from the failure path. It is now rejected atclient.resource(...), and again insideAuthplaneResourceso a resource built without the factory is covered too;build_prm_urlkeeps the check as a backstop.InvalidIssuerErrorandInvalidResourceErrorsubclass bothAuthplaneErrorandValueError, soexcept ValueErrorhandlers are unaffected while callers can now distinguish an identifier misconfiguration from any otherValueErrorthe SDK raises.build_prm_urlandbuild_metadata_urlno longer strip slashes from the front of the path:str.strip("/")removed them from both ends, so//mcplost a segment and derived the same well-known URL as/mcp— two distinct identifiers collapsing onto one document.Migration: remove any fragment from a resource indicator (RFC 8707 §2 forbids one), and correct any issuer that differs from the AS's published identifier by a trailing slash.
DPoP
htuand the;paramssegmenturlparsepeels an RFC 3986;paramssegment off the last path segment, so a request to/orders;v=2bound anhtuof/orders— a proof computed over a different resource than the one being accessed. RFC 3986 §3.3 puts;paramsin the path; the construction now usesurlsplit, which leaves it there.Verbatim PRM
VerbatimPRMRemoteAuthProviderbecomes public andrewrite_prm_routes_verbatimis exported: building aRemoteAuthProviderby hand is a documented FastMCP pattern and doing so silently lost the verbatim PRM.install_request_context(mcp)now warns when the verifier carries no verbatim identifiers instead of skipping the rewrite in silence — a verifier built through the public constructor has none, so the previous check never fired for it and the served document kept advertising slash-normalized identifiers that this SDK's own comparison rejects. It also stops readingmcp.sse_appunguarded, so a futuremcp1.x that drops the attribute cannot take down servers that never touch SSE.Security and tooling
The
mcpdependency floor moves to>=1.28.1, clearing PYSEC-2026-3483;authplane-fastmcpdeclares it directly, since it imports the top-levelmcppackage and the transitivefastmcpdependency does not guarantee the floor.verify_dpop_proofno longer echoes the expected or received nonce into its error —www_authenticatecopies the message verbatim intoerror_description, which would hand an unauthenticated caller a currently-valid resource-server nonce.The conformance catalog is fetched at a pinned SHA with a drift check, and
uv.lockfiles are ignored: nothing tracks or consumes them, and three were once swept into a PR as untracked files accounting for 94% of its diff.