Fix 500 on /login when a Kratos session exists but no app JWT (session_already_available)

Repro: register a new account, then click back from the password/verification step to /login →
500. Registration's `session` hook signs the user in at Kratos but routes to the verification UI,
not /auth/complete, so they hold a Kratos session with NO app JWT — ctx.user is null, the "already
signed in -> /dashboard" short-circuit can't fire, and initialising a login flow makes Kratos return
400 `session_already_available`. The flow-init catch only handled 403/404/410 and 5xx, so the 400
fell through to `throw` -> catch-all 500.

Recover instead: on `session_already_available` (already authenticated at Kratos), 303 to
/auth/complete to mint the JWT from the live session, preserving return_to. A genuinely unexpected
Kratos 400 still surfaces as 500. Verified live: /login (Kratos session, no JWT) now 303s to
/auth/complete, which mints plainpages_jwt and lands on /dashboard.

Also default LOG_LEVEL to debug in the dev override (compose.override.yml) for verbose local logs.

Tests-first: app.test asserts the session-race recovers to /auth/complete (return_to carried) while
an unrelated 400 stays a 500. typecheck + 361 units green.
This commit is contained in:
2026-06-21 22:04:55 +02:00
parent 1d198acc97
commit c8b4c3c23b
3 changed files with 37 additions and 0 deletions
+10
View File
@@ -305,6 +305,16 @@ export function createApp(options: AppOptions = {}): Server {
res.writeHead(303, { location: pathname }).end();
return;
}
// Already authenticated at Kratos but no app JWT yet (e.g. straight after registration, whose
// `session` hook signs the user in but routes to verification, not /auth/complete — so ctx.user
// is null and the "already signed in" short-circuit above can't fire). Initialising a login/
// registration flow then returns Kratos 400 `session_already_available`. Recover by completing
// login (mint the JWT from the live session), honouring return_to — never a 500.
if (err instanceof KratosError && err.status === 400 && err.body.includes("session_already_available")) {
const local = localPath(ctx.url.searchParams.get("return_to"));
res.writeHead(303, { location: local ? `/auth/complete?return_to=${encodeURIComponent(local)}` : "/auth/complete" }).end();
return;
}
// Ory unreachable (Kratos 5xx / connection refused / timeout): "Ory down ⇒ no logins" is
// documented, so render an honest 503 rather than the catch-all "error on our end" 500.
if (!(err instanceof KratosError) || err.status >= 500) {