Symptom
GET /idp/interaction/<uid>/schnorr-complete?accountId=<id> hangs and the gateway times out with 504. JSS log shows:
```
Schnorr login completed (accountId=b0b1707f-... uid=_T_qlTgrG3zS1...)
Promise errored, but reply.sent = true was set
err: SessionNotFound: invalid_request — interaction session id cookie not found
at #getInteraction (oidc-provider/lib/provider.js:405)
at Provider.interactionResult (provider.js:199)
at Provider.interactionFinished (provider.js:217)
at handleSchnorrComplete (src/idp/interactions.js:833)
```
The connection hangs because the handler called `reply.hijack()` before `provider.interactionFinished(...)`, so when `interactionFinished` throws, Fastify can't send the error and the socket sits open until the gateway times it out.
Reproduction
Anything that breaks `_interaction` cookie continuity between the OIDC `/auth` step and `/schnorr-complete`:
- Start the OIDC flow in browser A, copy-paste the interaction URL into browser B (today's repro)
- Third-party cookie blocking on a cross-site IdP/RP boundary
- Incognito → main session
- The same flow after a long idle (cookie expired but interaction record still findable)
Cause
`handleSchnorrComplete` (`src/idp/interactions.js:805`) does the right thing structurally — looks up the interaction by `uid`, validates the accountId — and then calls `provider.interactionFinished(request.raw, reply.raw, interaction.result, ...)`. `interactionFinished` internally re-reads the `_interaction` cookie via `#getInteraction` and explodes when it's missing, even though we already proved we have the right interaction.
Two compounding issues:
- Unnecessary cookie dependency. We have the interaction object in hand. The cookie check is redundant for this code path.
- Hijack-then-throw silently hangs. `reply.hijack()` is called BEFORE `interactionFinished`. If `interactionFinished` throws, Fastify's `Promise errored, but reply.sent = true was set` warning fires and nobody writes a response. Connection hangs → 504.
Fix sketches
A. Move the OIDC finalize into `handleSchnorrLogin`. The login handler already saves `interaction.result` and has the `_interaction` cookie in flight (it's still the same browser, same request). After saving, it can call `provider.interactionFinished(request.raw, reply.raw, result, {...})` and return its redirect target as `redirectUrl` in the JSON. Drop `schnorr-complete` entirely. This is the cleanest fix because it removes the cookie-dependency window.
B. Keep `schnorr-complete` but bypass `interactionFinished`. We have the saved `interaction.result` and can directly construct the OIDC continue URL (`interaction.returnTo`) ourselves. Redirect the browser there and oidc-provider's auth endpoint picks up the saved result without needing the cookie.
C. Defensive: don't `reply.hijack()` until AFTER `interactionFinished` resolves. At minimum this turns a 504 hang into a clean 4xx so users get a real error.
A or B fix the actual flow. C should land regardless.
Refs
Symptom
GET /idp/interaction/<uid>/schnorr-complete?accountId=<id>hangs and the gateway times out with 504. JSS log shows:```
Schnorr login completed (accountId=b0b1707f-... uid=_T_qlTgrG3zS1...)
Promise errored, but reply.sent = true was set
err: SessionNotFound: invalid_request — interaction session id cookie not found
at #getInteraction (oidc-provider/lib/provider.js:405)
at Provider.interactionResult (provider.js:199)
at Provider.interactionFinished (provider.js:217)
at handleSchnorrComplete (src/idp/interactions.js:833)
```
The connection hangs because the handler called `reply.hijack()` before `provider.interactionFinished(...)`, so when `interactionFinished` throws, Fastify can't send the error and the socket sits open until the gateway times it out.
Reproduction
Anything that breaks `_interaction` cookie continuity between the OIDC `/auth` step and `/schnorr-complete`:
Cause
`handleSchnorrComplete` (`src/idp/interactions.js:805`) does the right thing structurally — looks up the interaction by `uid`, validates the accountId — and then calls `provider.interactionFinished(request.raw, reply.raw, interaction.result, ...)`. `interactionFinished` internally re-reads the `_interaction` cookie via `#getInteraction` and explodes when it's missing, even though we already proved we have the right interaction.
Two compounding issues:
Fix sketches
A. Move the OIDC finalize into `handleSchnorrLogin`. The login handler already saves `interaction.result` and has the `_interaction` cookie in flight (it's still the same browser, same request). After saving, it can call `provider.interactionFinished(request.raw, reply.raw, result, {...})` and return its redirect target as `redirectUrl` in the JSON. Drop `schnorr-complete` entirely. This is the cleanest fix because it removes the cookie-dependency window.
B. Keep `schnorr-complete` but bypass `interactionFinished`. We have the saved `interaction.result` and can directly construct the OIDC continue URL (`interaction.returnTo`) ourselves. Redirect the browser there and oidc-provider's auth endpoint picks up the saved result without needing the cookie.
C. Defensive: don't `reply.hijack()` until AFTER `interactionFinished` resolves. At minimum this turns a 504 hang into a clean 4xx so users get a real error.
A or B fix the actual flow. C should land regardless.
Refs