Skip to content

fix(idp): RFC 9207 iss param matches discovery issuer (trailing-slash normalize in createProvider) — closes #524 - #551

Merged
melvincarvalho merged 1 commit into
gh-pagesfrom
issue-524-iss-trailing-slash
Jun 10, 2026
Merged

fix(idp): RFC 9207 iss param matches discovery issuer (trailing-slash normalize in createProvider) — closes #524#551
melvincarvalho merged 1 commit into
gh-pagesfrom
issue-524-iss-trailing-slash

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Closes #524.

Bug

The discovery handler normalizes the issuer field of /.well-known/openid-configuration to the trailing-slash form ("CTH compatibility", src/idp/index.js:223), but oidc-provider was constructed with the raw configured issuer (src/idp/provider.js). The provider's issuer feeds the RFC 9207 iss authorization-response parameter and the iss claim in issued tokens — so with an issuer configured slash-free:

discovery issuer  → http://host:port/
callback iss      → http://host:port

RFC 9207 requires byte-identity. Strict clients — solid-oidc's handleRedirectFromLogin does exactly this comparison — rejected the callback before the token request fired, so sign-in silently bounced with no session. Reproduced on Android/nodejs-mobile (#522), where it was the final blocker to a working on-device login.

Fix (1 functional line)

Normalize inside createProvider with the same expression the discovery handler uses:

issuer = issuer.endsWith('/') ? issuer : issuer + '/';

Both normalization sites now carry bidirectional sync comments naming each other and #524, so neither can drift silently.

Blast radius — verified before landing

Concern Finding
Internal token verification Slash-tolerant by design: addTrustedIssuer (solid-oidc.js:36) and getOidcConfig (:272,288) strip trailing slashes; JWKS verification uses the token's own iss (self-consistent)
Discovery doc Unchanged — idp.test.js:62 keeps asserting issuer === baseUrl + '/'
Programmatic credentials tokens (credentials.js:119 stamps raw iss) Deliberately not touched — those tokens have their own verification path and no RFC 9207 involvement; changing their iss risks breakage for zero benefit here
Full suite 937/937

Tests

New test/idp-issuer-normalization.test.js (3 cases):

  1. Slash-free issuer → provider issuer gains the slash
  2. Already-slashed issuer → passes through unchanged
  3. Cross-component pin: provider.issuer byte-equals the issuer field fetched from a live server's discovery endpoint — the exact comparison strict clients perform. If either normalization drifts, this test goes red.

Refs

…createProvider (#524)

The discovery handler normalizes the `issuer` field of
/.well-known/openid-configuration to the trailing-slash form, but
oidc-provider was constructed with the RAW configured issuer. The
provider's issuer feeds the RFC 9207 `iss` authorization-response
parameter and the `iss` claim in issued tokens, so with an issuer
configured slash-free:

  discovery issuer  → http://host:port/
  callback iss      → http://host:port

RFC 9207 requires byte-identity; strict clients (solid-oidc's
handleRedirectFromLogin) rejected the callback BEFORE the token
request fired and sign-in silently bounced. Reproduced on Android /
nodejs-mobile (#522), where it was the final blocker to a working
on-device login.

Fix: normalize inside createProvider with the same expression the
discovery handler uses, with bidirectional sync comments on both
sites so neither normalization can drift silently.

Blast-radius notes (verified before landing):
  - Internal verification is slash-tolerant: addTrustedIssuer and
    getOidcConfig strip trailing slashes; JWKS verification uses the
    token's own iss (self-consistent).
  - The discovery doc is unchanged (idp.test.js:62 keeps asserting
    issuer === baseUrl + '/').
  - handleCredentials' programmatic-token iss stamp (credentials.js:119)
    is deliberately NOT touched: those tokens have their own
    verification path and no RFC 9207 involvement.

Tests (test/idp-issuer-normalization.test.js, 3 cases): slash-free
issuer gains the slash, already-slashed passes through, and the
cross-component pin — provider.issuer byte-equals the discovery
issuer fetched from a running server, the exact comparison strict
clients perform.

Full suite: 937/937 passing.

Closes #524.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an IdP issuer mismatch where discovery normalizes issuer to a trailing-slash form but oidc-provider was instantiated with the raw configured issuer, causing RFC 9207 iss (auth response + token claim) to differ byte-for-byte from discovery and break strict OIDC clients.

Changes:

  • Normalize the issuer inside createProvider() to always use the trailing-slash canonical form.
  • Add/extend bidirectional comments to keep discovery and provider issuer normalization logic synchronized.
  • Add a dedicated regression test suite that pins provider issuer normalization and asserts provider issuer exactly equals discovery’s issuer field.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/idp/provider.js Normalizes the provider’s issuer to trailing-slash form so RFC 9207 iss matches discovery.
src/idp/index.js Updates discovery handler comment to explicitly sync with provider normalization and RFC 9207 requirements.
test/idp-issuer-normalization.test.js Adds regression coverage for slash normalization and cross-component discovery/provider issuer equality.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@melvincarvalho
melvincarvalho merged commit 80a42aa into gh-pages Jun 10, 2026
2 checks passed
@melvincarvalho
melvincarvalho deleted the issue-524-iss-trailing-slash branch June 10, 2026 19:24
melvincarvalho added a commit that referenced this pull request Jun 11, 2026
Eight PRs merged since 0.0.206 — IdP hardening, protocol conformance,
and the de-Googled-phone (#46) arc:

IdP / auth
- #558 passkey login degrades cleanly on stale WebViews / insecure
  contexts instead of crashing — drops a redundant browser
  crypto.randomUUID and guards both ceremonies on secure-context +
  WebAuthn (closes #556)
- #554 IdP login-form error now survives the POST→redirect→GET cycle
  (rode in a non-persisted field that Interaction.save() dropped);
  failed sign-ins show the error instead of a silent re-render
  (closes #514)
- #551 RFC 9207 'iss' authorization-response param normalized to match
  the discovery issuer, so strict OIDC clients (solid-oidc) complete
  sign-in (closes #524)

Tunnel
- #555 opt-in, per-tunnel credential passthrough (Cookie /
  Authorization / Set-Cookie) so authenticated access works through a
  tunnel; the relay's own IdP session cookies are isolated from the
  tunnel client (closes #530)

Content negotiation / git
- #553 HEAD now mirrors GET's negotiated Content-Type / Content-Length
  / Cache-Control for files (RFC 9110 §9.3.2 parity) (closes #552)
- #550 git WAC preHandler 401/402/403 responses carry the git CORS
  headers, so browser git clients see the status, not a CORS error
  (closes #548)
- #549 first HTTP-contract coverage for the git handler + fixes a
  DATA_ROOT test-pollution bug (closes #375)

Docs / metadata
- #547 README tagline + npm keywords surface the agentic positioning
  (closes #406)
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.

RFC 9207 iss param != discovery issuer (trailing-slash mismatch) breaks strict OIDC clients

2 participants