Skip to content

fix(idp): login error survives the redirect — ride in lastSubmission — closes #514 - #554

Merged
melvincarvalho merged 2 commits into
gh-pagesfrom
issue-514-login-error-render
Jun 10, 2026
Merged

fix(idp): login error survives the redirect — ride in lastSubmission — closes #514#554
melvincarvalho merged 2 commits into
gh-pagesfrom
issue-514-login-error-render

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Closes #514.

Bug — and why it was invisible in the source

Every piece of the error wiring looked correct: the failed-login path set interaction.lastError and saved (interactions.js:178/186), the GET re-render passed it to the view (:98), and loginPage() renders the error div (views.js:448). Yet the live repro confirmed the issue: wrong password → redirect → pristine form, no error.

Root cause: oidc-provider's Interaction.save() persists only the fields in the model's IN_PAYLOAD list (node_modules/oidc-provider/lib/models/interaction.js:57). lastError is a foreign property — it survived in memory, was silently dropped on save, and the redirected GET loaded a fresh instance without it.

Fix

The message rides in lastSubmission — the IN_PAYLOAD slot oidc-provider designates for exactly this form re-render state:

Site Change
interactions.js:178 (missing fields) interaction.lastSubmission = { lastError: … } + the IN_PAYLOAD gotcha documented in a comment
interactions.js:186 (bad credentials) same, with a pointer to the note
interactions.js:98 (GET re-render) reads interaction.lastSubmission?.lastError
interactions.js:407 (switch-account clear) clears lastSubmission

Verification

  • Live before/after: the issue's exact repro (single-user IdP, wrong password) — before: no error div in the re-rendered HTML; after: <div class="error">Invalid username or password</div> renders.
  • Regression test (test/idp-login-error.test.js): drives the real OIDC interaction flow over HTTP — dynamic client registration → /idp/auth → interaction redirect → POST bad credentials → follow redirect — with manual cookie threading, asserting the error div appears. DATA_ROOT snapshot/restore per house pattern.
  • Full suite: 948/948 passing.

On the issue's "sweep other pages" note

The register flow renders registerPage(uid, error, …) directly in the same response (no save/redirect cycle), so it doesn't have this bug — the persistence trap only fires when error state must survive a round-trip through Interaction.save(). The login form was the only such site (the lastError grep now returns zero hits).

…514)

The failed-login path set `interaction.lastError`, saved, and
redirected back to the form. Every piece of the wiring looked right —
views.js renders the error div, the GET re-render passes the value,
the setters fire — but the error never appeared. Root cause:
oidc-provider's Interaction.save() persists ONLY the fields in the
model's IN_PAYLOAD list (lib/models/interaction.js), and `lastError`
isn't one of them. The property survived in memory, was silently
dropped on save, and the redirected GET re-rendered a pristine form.
Users retried blind; the issue author lost hours to an imagined OIDC
regression (#513) because the IdP refused to surface a wrong-password
error.

Fix: the message rides in `lastSubmission` — the IN_PAYLOAD slot
oidc-provider designates for exactly this form re-render state. All
four sites updated (two setters, the GET reader, and the
switch-account clear), with the IN_PAYLOAD gotcha documented at the
first setter so the next foreign-property bug doesn't take hours to
spot.

Verified live before and after: the issue's repro (wrong password on
a single-user IdP) now renders
`<div class="error">Invalid username or password</div>` on the
re-rendered form.

Regression test drives the real OIDC interaction flow over HTTP
(dynamic client registration → /idp/auth → interaction redirect →
POST bad credentials → follow redirect) with manual cookie threading
and asserts the error div. 948/948 passing.

Closes #514.

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 IdP login error messages being lost across the POST→redirect→GET cycle by storing the error in Interaction.lastSubmission (an oidc-provider persisted payload field) and adds a regression test for the full interaction flow over HTTP.

Changes:

  • Read login error state from interaction.lastSubmission?.lastError when rendering the login page.
  • Write login validation/authentication errors into interaction.lastSubmission (instead of a non-persisted interaction.lastError) and clear it on switch-account.
  • Add an end-to-end test that reproduces the failed-login redirect and asserts the error banner is rendered.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/idp/interactions.js Persists login error state through oidc-provider interaction saves via lastSubmission and clears it on account switch.
test/idp-login-error.test.js New regression test covering the real OIDC interaction redirect loop and ensuring the login error banner appears.

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

Comment on lines +41 to +47
// Collect cookies from a response and merge into a name→value jar.
function absorbCookies(jar, res) {
for (const c of res.headers.getSetCookie?.() || []) {
const [pair] = c.split(';');
const eq = pair.indexOf('=');
if (eq > 0) jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a7ea839 — with an explicit guard rather than a quiet fallback: the test detects Headers.getSetCookie support up front and skips with a self-documenting reason on older Node. Context that makes the skip free: the IdP itself cannot run on Node 18 at all (oidc-provider uses Array#toReversed and crypto.hash#523, engines bump deferred as #541), so every IdP test is already broken on those runtimes; this one now states why instead of failing at a confusing distance. On supported runtimes the jar call is now direct (no optional chaining) since the guard owns the decision. Suite still green.

…-cookie failure

Copilot: Headers.getSetCookie() landed in Node 18.15/19.7, but
engines still declares >=18.0.0 (bump deferred — #541). The previous
optional-chaining fallback would collect zero cookies on older Node
and fail the test at a confusing distance.

Fix: detect support up front and skip with a self-documenting reason.
Skipping costs nothing on those runtimes — the IdP itself cannot run
on Node 18 at all (oidc-provider uses Array#toReversed and
crypto.hash, #523), so every IdP test is already broken there; this
one now says WHY instead of failing mysteriously. The jar call is now
direct (no optional chaining) since the guard owns the decision.

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

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

@melvincarvalho
melvincarvalho merged commit a38a9e1 into gh-pages Jun 10, 2026
2 checks passed
@melvincarvalho
melvincarvalho deleted the issue-514-login-error-render branch June 10, 2026 21:09
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.

IdP login form re-renders silently on failed authentication — error not shown to user

2 participants