Skip to content

fix(sdk): stop rewriting issuer and resource identifiers - #47

Closed
RobertoIskandarani wants to merge 1 commit into
mainfrom
fix/identifier-identity-trailing-slash
Closed

fix(sdk): stop rewriting issuer and resource identifiers#47
RobertoIskandarani wants to merge 1 commit into
mainfrom
fix/identifier-identity-trailing-slash

Conversation

@RobertoIskandarani

Copy link
Copy Markdown
Contributor

What

RFC 8414 §3.3 and RFC 9728 §3.3 require the advertised issuer/resource to be identical to the configured value ("If these values are not identical, the data contained in the response MUST NOT be used") — a simple string comparison that exists to defeat metadata substitution. Both well-known URLs are formed by inserting the well-known path segment between the authority and the identifier's path (RFC 8414 §3 / RFC 9728 §3), with no normalisation step. The SDK instead stripped trailing slashes in four places; this PR removes all four and replaces silent rewriting with construction-time validation.

The user-visible bug

AuthplaneClient.create rewrote the configured issuer (options.issuer.replace(/\/+$/g, "")), and that rewritten value became jose's expected issuer at token verification. For an authorization server whose issuer identifier legitimately ends in /, RFC 9068 requires the token's iss to carry the trailing slash — so every token was rejected for such a deployment. Verified by reintroducing the strip: the new end-to-end conformance variant fails (discovery resolves the wrong well-known URL), and passes with the fix.

The four sites

  • core/client.ts — issuer stored verbatim; validated, never rewritten.
  • core/prm.ts — the PRM document URL/path helpers stripped the resource path's trailing slash, serving /mcp where RFC 9728 §3 requires /mcp/. Now pure insertion.
  • core/fetching/metadataUrl.ts — pure insertion; issuer https://auth.example.com/tenant/ now resolves to /.well-known/oauth-authorization-server/tenant/.
  • core/fetching/documentCache.ts — issuer comparison is an exact string match on both sides; a document advertising https://as.example.com/ no longer passes against a configured https://as.example.com. (These two strips masked the client-level bug during discovery — fixed together so the symptom is removed, not moved.)

Validation instead of repair

New core/identifiers.ts: identifiers must be absolute http(s) URLs with an authority and no fragment (RFC 8707 §2 forbids fragments). Trailing slashes, host case, and explicit ports are legal variations and preserved verbatim. Applied at AuthplaneClient.create (issuer) and AuthplaneResource (resource).

Derivation is a pure string insertion via a shared split helper rather than WHATWG URL serialisation — the WHATWG URL Standard mandates serialising an empty path as / and rewrites path bytes, either of which would break the RFC 8414 §3 / RFC 9728 §3 requirement that the derived URL preserves the identifier exactly.

Conformance

  • rfc9728-well-known-path-must-derive-from-resource-uri — extended with the trailing-slash resource datum (/mcp//.well-known/oauth-protected-resource/mcp/).
  • rfc8414-metadata-issuer-must-match-configured-issuer — variant: metadata issuer differing from the configured one only by a trailing slash is rejected (equivalent per RFC 3986 §6.2.3 is not identical).
  • rfc9068-issuer-must-match — variant: a token whose iss is identical to a configured trailing-slash issuer verifies end to end, including discovery at the trailing-slash well-known URL (the mock AS can now serve metadata at a configurable path and advertise a suffixed issuer).

Migration

If your configured issuer or resource differs from your authorization server's actual identifier by a trailing slash, correct the config — the SDK no longer silently reconciles them. (CHANGELOG entry included; the previous Unreleased entry that documented trailing-slash normalisation as intended behaviour is superseded.)

Validation

@authplane/sdk: 382/382 tests green (unit + conformance suite against the amended catalog), typecheck, biome lint, arch check. Adapter packages unaffected by the changed files.

@RobertoIskandarani
RobertoIskandarani requested a review from a team as a code owner July 29, 2026 14:03
@RobertoIskandarani RobertoIskandarani self-assigned this Jul 29, 2026
RFC 8414 §3.3 and RFC 9728 §3.3 require the advertised issuer/resource
to be identical to the configured value — a simple string comparison —
and both well-known URLs are formed by inserting the well-known path
segment into the identifier verbatim (RFC 8414 §3 / RFC 9728 §3). The
SDK instead stripped trailing slashes in four places:

- AuthplaneClient.create rewrote the configured issuer. The rewritten
  value became the expected `iss` at token verification, so an AS whose
  issuer identifier legitimately ends in "/" had every token rejected
  (RFC 9068 requires `iss` to carry the slash).
- The PRM document URL/path helpers stripped the resource path's
  trailing slash, serving /mcp where RFC 9728 §3 requires /mcp/.
- buildMetadataUrl stripped the issuer path's trailing slash against
  the RFC 8414 §3 insertion rule.
- MetadataCache stripped both sides of the issuer comparison, weakening
  the §3.3 identical-match MUST that defeats metadata substitution.

Identifiers are now validated at construction instead (absolute
http(s) URL with an authority, no fragment — RFC 8707 §2) and never
transformed; derivation is pure string insertion via a shared helper,
since WHATWG URL serialises an empty path as "/" and cannot preserve
the identifier exactly.

Conformance: extends the rfc9728 well-known-path case with the
trailing-slash resource datum and adds the two issuer variants
(metadata issuer differing only by a trailing slash is rejected; a
token whose iss matches a trailing-slash issuer verifies end to end).

Migration: if a configured issuer or resource differs from the
authorization server's actual identifier by a trailing slash, correct
the config — the SDK no longer silently reconciles them.
@RobertoIskandarani
RobertoIskandarani force-pushed the fix/identifier-identity-trailing-slash branch from 8f8a77c to 3bc2afa Compare July 29, 2026 14:08
@RobertoIskandarani
RobertoIskandarani requested review from muralx and removed request for a team and muralx July 29, 2026 14:10
@RobertoIskandarani

Copy link
Copy Markdown
Contributor Author

Closing — superseded. The identifier-handling change is being reworked against the corrected RFC 8414 §3.1 / RFC 9728 §3.1 (derivation strips the terminating slash) vs §3.3 (identity preserved verbatim) scope. A replacement PR will follow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant