From 39eb9e039adc4366280614a7f04fbb07b5aecb63 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Wed, 15 Apr 2026 18:26:51 +0100 Subject: [PATCH] fix(auth): auto-submit OIDC logout form to eliminate xsrf desync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users clicking sign-out in the wiki were getting 'xsrf token invalid'. The old logoutSource extracted the xsrf from oidc-provider's form into a separate short-lived cookie and bounced through /auth/logout-confirm, but that dance kept desyncing — the xsrf on the eventual submit didn't always match the session state on /oidc/session/end/confirm. Drop the custom confirmation page and auto-submit oidc-provider's own form inline from logoutSource. The xsrf stays inside the original form HTML the provider generated, so the validation is guaranteed to match. Clicking sign-out in the wiki is already confirmation enough. Also clear the Ghost Guild auth-token cookie in postLogoutSuccessSource so signing out of the wiki fully signs the user out rather than leaving a stale ghostguild.org session behind. --- server/utils/oidc-provider.ts | 53 ++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/server/utils/oidc-provider.ts b/server/utils/oidc-provider.ts index 25edceb..dfc7042 100644 --- a/server/utils/oidc-provider.ts +++ b/server/utils/oidc-provider.ts @@ -93,34 +93,43 @@ export async function getOidcProvider() { rpInitiatedLogout: { enabled: true, logoutSource: async (ctx: any, form: string) => { - // oidc-provider's form HTML is a stable format (see node_modules/ - // oidc-provider/lib/actions/end_session.js:90): - //
- // We extract just the xsrf token and hand off to a Nuxt page at - // /auth/logout-confirm that renders a styled form posting back to - // /oidc/session/end/confirm with that xsrf value. The token rides - // in a short-lived httpOnly cookie so it never hits the URL. - const match = form.match(/name="xsrf"\s+value="([^"]+)"/); - if (!match) { - // Defensive: if oidc-provider ever changes its form format, fall - // back to the raw form so logout still works. - ctx.type = "html"; - ctx.status = 200; - ctx.body = `${form}`; - return; - } - ctx.cookies.set("oidc_logout_xsrf", match[1], { + // Auto-submit oidc-provider's own form so the xsrf value stays + // inside the same request cycle that generated it. The previous + // approach extracted the xsrf into a separate cookie and bounced + // through a Nuxt page for a "are you sure?" confirmation, which + // kept desyncing and producing "xsrf token invalid" errors. + // Clicking sign-out in the wiki is already confirmation enough. + ctx.type = "html"; + ctx.status = 200; + ctx.body = ` + + + Signing out — Ghost Guild + + + + +

Signing you out…

+ ${form} + + +`; + }, + postLogoutSuccessSource: async (ctx: any) => { + // Kill the Ghost Guild session cookie so the user is fully signed + // out, not just logged out of Outline. + ctx.cookies.set("auth-token", null, { httpOnly: true, sameSite: "lax", - maxAge: 120_000, // 2 minutes path: "/", overwrite: true, signed: false, }); - ctx.redirect("/auth/logout-confirm"); - }, - postLogoutSuccessSource: async (ctx: any) => { ctx.redirect("/auth/logout-success"); }, },