Skip to content

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 27 Aug 20:01
· 3 commits to main since this release

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.New and authplane.NewClient all route through it.
  • core/resource/verifier: ErrInvalidIssuer sentinel, returned by everything that validates an issuer identifier. Match it with errors.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=…), and net/url.Error prints 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, so errors.As(err, new(*url.Error)) keeps working; only the URL the error prints is substituted.
  • http: the RFC 9728 PRM discovery bypass in the net/http adapter now compares r.URL.EscapedPath() against the escaped well-known path instead of the decoded r.URL.Path. A resource identifier carrying a percent-encoded octet (e.g. %2F) yields an escaped well-known path; comparing the decoded path let %2F collapse 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: NewTokenVerifier and resource.New now 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.ParseRequestURI alone accepted both: it takes a path-only reference, and it folds a fragment into Path rather than splitting it. Migration: pass the authorization server's issuer identifier exactly as published — absolute, https, no query, no fragment.
  • BREAKING core/authplane: NewClient additionally 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 *Client used only for token, introspection and revocation calls never constructs a TokenVerifier, so it is the only thing keeping a relative reference out of eager discovery. Migration: as above.
  • BREAKING core/authplane: ErrInvalidIssuer is now an alias of verifier.ErrInvalidIssuer rather than its own sentinel. Two consequences for code that inspects it: the message changes from authplane: invalid issuer to verifier: invalid issuer, and errors.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 did errors.Is(err, authplane.ErrInvalidIssuer) on a NewClient error is unaffected.
  • BREAKING core/authplane: NewClient now 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 to NewClient; an issuer identifier never carries one.
  • BREAKING core/resource: resource.New now rejects a resource URI containing a # (RFC 8707 §2 forbids a fragment in a resource indicator). url.ParseRequestURI does not split the fragment, so https://api.example.com/mcp#frag previously 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 to resource.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/mcp rather 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() and PRMURL() 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 as https://api.example.com/mcp%2Fx therefore yields .../oauth-protected-resource/mcp%2Fx where 0.2.0 returned .../mcp/x — a visible output change on both exported methods. Migration: if you consume these values (routing the PRM handler, advertising resource_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's issuer byte-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 at NewClient as metadata: 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 to NewTokenVerifier verbatim and matches a token's iss claim byte-for-byte (RFC 8414 §4: code-point-for-code-point, no normalization) instead of trailing-slash-insensitively. A token whose iss differs from the configured issuer only by a trailing slash is now an ErrIssuerMismatch. Migration: If the issuer you pass to NewTokenVerifier differs from your authorization server's actual identifier by a trailing slash, correct it — the SDK no longer silently reconciles them.