v0.3.0
Released commit: 841793dbe22bfeb36456ee760f9fb2a201d64ab9
Added
core/resource/verifier:ValidateIssuer(issuer string) error— the RFC 8414 §2 issuer-shape rule, exported so every construction boundary applies one implementation rather than a copy. Rejects a query or fragment component, and requires an absolute URL with a scheme and host.NewTokenVerifier,resource.Newandauthplane.NewClientall route through it.core/resource/verifier:ErrInvalidIssuersentinel, returned by everything that validates an issuer identifier. Match it witherrors.Is.
Fixed
core/resource/verifier,core/authplane: an issuer rejected at construction is no longer echoed verbatim into the error. The query/fragment branch fires for exactly the shape that can carry a credential (https://as.example.com?access_token=…), andnet/url.Errorprints its URL field without redacting, so the raw identifier — query, fragment and any userinfo — reached whatever log the construction error landed in. Messages now carry scheme and host only. Parse failures are still wrapped with%w, soerrors.As(err, new(*url.Error))keeps working; only the URL the error prints is substituted.http: the RFC 9728 PRM discovery bypass in thenet/httpadapter now comparesr.URL.EscapedPath()against the escaped well-known path instead of the decodedr.URL.Path. A resource identifier carrying a percent-encoded octet (e.g.%2F) yields an escaped well-known path; comparing the decoded path let%2Fcollapse to/, the two sides disagreed, and the discovery endpoint stopped being bypassed and returned 401 even though RFC 9728 §3.2 requires it publicly reachable. The check is deliberately stricter than RFC 3986 §6.2.2.1 (a percent-encoded unreserved octet won't match its decoded form), an accepted trade-off since a conformant client signs the same octets the operator configured.
Changed
- BREAKING
core/resource/verifier,core/resource:NewTokenVerifierandresource.Newnow reject an issuer carrying a query or fragment component, and require the identifier to be an absolute URL with a scheme and host (RFC 8414 §2). Construction that succeeded in 0.2.0 — a relative reference such as/tenant, or an issuer with?x=1— now fails.url.ParseRequestURIalone accepted both: it takes a path-only reference, and it folds a fragment intoPathrather than splitting it. Migration: pass the authorization server's issuer identifier exactly as published — absolute,https, no query, no fragment. - BREAKING
core/authplane:NewClientadditionally requires the issuer to be absolute with a scheme and host, beyond the query/fragment rule below. This gate is not redundant with the verifier's: a*Clientused only for token, introspection and revocation calls never constructs aTokenVerifier, so it is the only thing keeping a relative reference out of eager discovery. Migration: as above. - BREAKING
core/authplane:ErrInvalidIssueris now an alias ofverifier.ErrInvalidIssuerrather than its own sentinel. Two consequences for code that inspects it: the message changes fromauthplane: invalid issuertoverifier: invalid issuer, anderrors.Is(err, authplane.ErrInvalidIssuer)now returns true for a rejection raised by the verifier, where it previously returned false. Migration: if you relied on the two sentinels being distinct to tell which layer rejected an identifier, that distinction is gone — both boundaries now apply the same rule, so match on the single sentinel and read the message for the specific violation. Code that only diderrors.Is(err, authplane.ErrInvalidIssuer)on aNewClienterror is unaffected. - BREAKING
core/authplane:NewClientnow rejects an issuer containing a query or fragment component (RFC 8414 §2 forbids both) instead of passing it straight into metadata discovery. Previously the resource side rejected a fragment but the issuer had no such check, and the two discovery-URL builders diverged when either was present — the RFC 8414 builder silently dropped the issuer's query/fragment while the OIDC builder carried them along, so the two discovery attempts targeted different identities. Construction now fails immediately with a clear error. Migration: strip any query or fragment from the issuer you pass toNewClient; an issuer identifier never carries one. - BREAKING
core/resource:resource.Newnow rejects a resource URI containing a#(RFC 8707 §2 forbids a fragment in a resource indicator).url.ParseRequestURIdoes not split the fragment, sohttps://api.example.com/mcp#fragpreviously passed the scheme/host check and leaked the fragment into the derived PRM URL. This is a construction-time change on the exported constructor. Migration: remove any fragment from the resource URI you pass toresource.New. - BREAKING
core/resource: the RFC 9728 §3.1 PRM well-known URL now strips any terminating slash following the host component before inserting the well-known path suffix, so a resource identifier ending in/mcp/is served at (and derived by a conformant client as)/.well-known/oauth-protected-resource/mcprather than.../mcp/. The resource identifier itself is unchanged — only the derived publication URL loses the slash. Migration: if you currently serve your PRM document at a trailing-slash well-known path, move it to the slash-stripped path (or route both) so RFC 9728 clients stop 404ing. - BREAKING
core/resource:WellKnownPRMPath()andPRMURL()now derive from the resource identifier's escaped path, so a percent-encoded octet (RFC 3986 §3.3 path data, e.g.%2F) is carried through verbatim instead of being decoded to/. A resource identifier such ashttps://api.example.com/mcp%2Fxtherefore yields.../oauth-protected-resource/mcp%2Fxwhere 0.2.0 returned.../mcp/x— a visible output change on both exported methods. Migration: if you consume these values (routing the PRM handler, advertisingresource_metadata), ensure your router matches the escaped path. - BREAKING
core/internal/metadata: the RFC 8414 §3.3 issuer check now compares the configured issuer and the metadata document'sissuerbyte-for-byte (§4: code-point-for-code-point, no normalization) instead of trailing-slash-insensitively. A document whose issuer differs from the configured issuer only by a trailing slash is now rejected as a mismatch. Because discovery is eager, this surfaces atNewClientasmetadata: issuer mismatch— construction fails immediately, not at the first token verification. Migration: If your configured issuer differs from your authorization server's actual identifier by a trailing slash, correct the config — the SDK no longer silently reconciles them. - BREAKING
core/resource/verifier: the token verifier stores the issuer passed toNewTokenVerifierverbatim and matches a token'sissclaim byte-for-byte (RFC 8414 §4: code-point-for-code-point, no normalization) instead of trailing-slash-insensitively. A token whoseissdiffers from the configured issuer only by a trailing slash is now anErrIssuerMismatch. Migration: If the issuer you pass toNewTokenVerifierdiffers from your authorization server's actual identifier by a trailing slash, correct it — the SDK no longer silently reconciles them.