appurl-canonical-host #1
@@ -12,6 +12,7 @@ services:
|
|||||||
APP_URL: ${APP_URL:-http://localhost:3000}
|
APP_URL: ${APP_URL:-http://localhost:3000}
|
||||||
CACHE_TEMPLATES: "false"
|
CACHE_TEMPLATES: "false"
|
||||||
LOG_FORMAT: "text" # human-readable logs in dev (base sets json for prod log pipelines)
|
LOG_FORMAT: "text" # human-readable logs in dev (base sets json for prod log pipelines)
|
||||||
|
LOG_LEVEL: "debug" # verbose by default while developing (base defaults to info)
|
||||||
REQUIRE_SECURE_SECRETS: "false"
|
REQUIRE_SECURE_SECRETS: "false"
|
||||||
SECURE_COOKIES: "false" # dev serves http — Secure cookies wouldn't be sent
|
SECURE_COOKIES: "false" # dev serves http — Secure cookies wouldn't be sent
|
||||||
SCHEDULING_UPSTREAM: "http://shifts-upstream:4000" # reference plugin → the dev mock backend
|
SCHEDULING_UPSTREAM: "http://shifts-upstream:4000" # reference plugin → the dev mock backend
|
||||||
|
|||||||
@@ -721,6 +721,32 @@ test("themed auth GET: anonymous inits a flow (CSRF relay, stale→restart); a s
|
|||||||
assert.equal((await fetch(url + "/settings", signedIn)).headers.get("location"), "/settings?flow=new1");
|
assert.equal((await fetch(url + "/settings", signedIn)).headers.get("location"), "/settings?flow=new1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("themed auth GET: an existing Kratos session (no app JWT yet) recovers via /auth/complete, never 500", async (t) => {
|
||||||
|
// After registration's `session` hook the user holds a Kratos session but no app JWT — so ctx.user
|
||||||
|
// is null and the "already signed in" short-circuit 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), preserving return_to — never fall through to the catch-all 500.
|
||||||
|
const sessionRace = new KratosError("Kratos init login flow failed (400)", 400, JSON.stringify({ error: { id: "session_already_available" } }));
|
||||||
|
const app = createApp({ jwks: staticJwks([ecJwk]), kratos: { ...mockKratos(async () => loginFlow("x")), initBrowserFlow: async () => { throw sessionRace; } } });
|
||||||
|
await new Promise<void>((r) => app.listen(0, r));
|
||||||
|
t.after(() => app.close());
|
||||||
|
const url = `http://localhost:${(app.address() as AddressInfo).port}`;
|
||||||
|
|
||||||
|
const recover = await fetch(url + "/login", { redirect: "manual" });
|
||||||
|
assert.equal(recover.status, 303);
|
||||||
|
assert.equal(recover.headers.get("location"), "/auth/complete");
|
||||||
|
// return_to is carried through so the deep link still lands after the JWT is minted.
|
||||||
|
const deep = await fetch(url + "/login?return_to=" + encodeURIComponent("/admin/users"), { redirect: "manual" });
|
||||||
|
assert.equal(deep.headers.get("location"), "/auth/complete?return_to=%2Fadmin%2Fusers");
|
||||||
|
|
||||||
|
// A genuinely unexpected Kratos 400 is still surfaced as a 500 (not masked as a session race).
|
||||||
|
const app2 = createApp({ jwks: staticJwks([ecJwk]), kratos: { ...mockKratos(async () => loginFlow("x")), initBrowserFlow: async () => { throw new KratosError("bad", 400, JSON.stringify({ error: { id: "security_csrf_violation" } })); } } });
|
||||||
|
await new Promise<void>((r) => app2.listen(0, r));
|
||||||
|
t.after(() => app2.close());
|
||||||
|
const url2 = `http://localhost:${(app2.address() as AddressInfo).port}`;
|
||||||
|
assert.equal((await fetch(url2 + "/login", { redirect: "manual" })).status, 500);
|
||||||
|
});
|
||||||
|
|
||||||
// return_to (§9): a deep-link login lands back on the requested page. The gate redirects to
|
// return_to (§9): a deep-link login lands back on the requested page. The gate redirects to
|
||||||
// /login?return_to=<host-relative path>; /login bakes that into the Kratos flow so completion
|
// /login?return_to=<host-relative path>; /login bakes that into the Kratos flow so completion
|
||||||
// returns there — but a first-party path must route via /auth/complete first (to mint the JWT).
|
// returns there — but a first-party path must route via /auth/complete first (to mint the JWT).
|
||||||
|
|||||||
+10
@@ -305,6 +305,16 @@ export function createApp(options: AppOptions = {}): Server {
|
|||||||
res.writeHead(303, { location: pathname }).end();
|
res.writeHead(303, { location: pathname }).end();
|
||||||
return;
|
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
|
// 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.
|
// documented, so render an honest 503 rather than the catch-all "error on our end" 500.
|
||||||
if (!(err instanceof KratosError) || err.status >= 500) {
|
if (!(err instanceof KratosError) || err.status >= 500) {
|
||||||
|
|||||||
Reference in New Issue
Block a user