From 8d1e8be6fcd2addd8c821894c08546840c14dadf Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 31 Oct 2026 09:10:57 +0100 Subject: [PATCH] =?UTF-8?q?Answer=20stability=20round=201:=200=20?= =?UTF-8?q?=F0=9F=94=B4,=202=20=F0=9F=9F=A1=20(0=20misread);=20pin=20the?= =?UTF-8?q?=20503=20on=20a=20non-401=20failure=20and=20the=20language-only?= =?UTF-8?q?=20return=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/http/app.test.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/http/app.test.ts b/src/http/app.test.ts index 81ae7e9..a3cbb59 100644 --- a/src/http/app.test.ts +++ b/src/http/app.test.ts @@ -793,11 +793,9 @@ test("themed auth GET: an existing Kratos session (no app JWT yet) recovers via }); test("themed auth GET: an anonymous hit on /settings (Kratos 401) bounces to sign-in with the page as return_to, never 500", async (t) => { - // /settings stays public (recovery lands there with a Kratos session but no app JWT), so Kratos' 401 - // for a settings flow with no session is the signal to sign in. /login itself must not redirect to - // itself, so a 401 there stays the catch-all 500. - const unauthorized = new KratosError("Kratos init settings flow failed (401)", 401, ""); - const app = createApp({ jwks: staticJwks([ecJwk]), kratos: { ...mockKratos(async () => { throw unauthorized; }), initBrowserFlow: async () => { throw unauthorized; } } }); + let failure = new KratosError("Kratos init settings flow failed (401)", 401, ""); + const throwing = async (): Promise => { throw failure; }; + const app = createApp({ jwks: staticJwks([ecJwk]), kratos: { ...mockKratos(throwing), initBrowserFlow: throwing } }); await new Promise((r) => app.listen(0, r)); t.after(() => app.close()); const url = `http://localhost:${(app.address() as AddressInfo).port}`; @@ -807,7 +805,11 @@ test("themed auth GET: an anonymous hit on /settings (Kratos 401) bounces to sig assert.equal(init.headers.get("location"), "/login?return_to=%2Fsettings"); const existing = await fetch(url + "/settings?flow=f1", { redirect: "manual" }); assert.equal(existing.headers.get("location"), "/login?return_to=%2Fsettings"); + // /login redirecting to itself would loop, so a 401 there stays the catch-all 500. assert.equal((await fetch(url + "/login", { redirect: "manual" })).status, 500); + // Only a 401 means "sign in": an Ory outage on /settings is still the honest 503. + failure = new KratosError("Kratos init settings flow failed (500)", 500, ""); + assert.equal((await fetch(url + "/settings", { redirect: "manual" })).status, 503); }); // return_to: a deep-link login lands back on the requested page. The gate redirects to @@ -838,6 +840,23 @@ test("login return_to: a first-party deep link is wrapped through /auth/complete assert.equal(lastReturnTo, "//evil.com"); }); +test("login return_to: a chosen language alone still routes through /auth/complete, so it survives the sign-in", async (t) => { + let lastReturnTo: string | undefined; + const kratos: KratosPublic = { + ...mockKratos(async (_t, id) => loginFlow(id)), + initBrowserFlow: async (_t: FlowType, opts = {}) => { lastReturnTo = opts.returnTo; return { flow: { id: "new1", ui: { action: "", method: "post", nodes: [] } }, setCookie: [] }; }, + }; + const app = createApp({ i18n: createI18n(await loadI18n()), kratos }); + await new Promise((r) => app.listen(0, r)); + t.after(() => app.close()); + const url = `http://localhost:${(app.address() as AddressInfo).port}`; + + await fetch(url + "/login?locale=sv-SE", { redirect: "manual" }); + assert.match(lastReturnTo ?? "", /^http:\/\/[^/]+\/auth\/complete\?locale=sv-SE$/); + await fetch(url + "/login?locale=sv-SE&return_to=" + encodeURIComponent("/admin/users"), { redirect: "manual" }); + assert.match(lastReturnTo ?? "", /^http:\/\/[^/]+\/auth\/complete\?return_to=%2Fadmin%2Fusers%3Flocale%3Dsv-SE&locale=sv-SE$/); +}); + // "Ory down ⇒ no logins" is documented; the auth path should say so honestly (503), not the // generic "error on our end" 500 the catch-all renders. test("auth flow when Ory is unreachable → an honest 503, not the catch-all 500", async (t) => {