fix(auth): auto-submit OIDC logout form to eliminate xsrf desync
Some checks failed
Test / vitest (push) Failing after 6m9s
Test / playwright (push) Has been skipped
Test / visual (push) Has been skipped
Test / Notify on failure (push) Successful in 2s

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.
This commit is contained in:
Jennie Robinson Faber 2026-04-15 18:26:51 +01:00
parent 3ad22a8b67
commit 39eb9e039a

View file

@ -93,34 +93,43 @@ export async function getOidcProvider() {
rpInitiatedLogout: { rpInitiatedLogout: {
enabled: true, enabled: true,
logoutSource: async (ctx: any, form: string) => { logoutSource: async (ctx: any, form: string) => {
// oidc-provider's form HTML is a stable format (see node_modules/ // Auto-submit oidc-provider's own form so the xsrf value stays
// oidc-provider/lib/actions/end_session.js:90): // inside the same request cycle that generated it. The previous
// <form id="op.logoutForm" method="post" action="..."><input // approach extracted the xsrf into a separate cookie and bounced
// type="hidden" name="xsrf" value="HEX"/></form> // through a Nuxt page for a "are you sure?" confirmation, which
// We extract just the xsrf token and hand off to a Nuxt page at // kept desyncing and producing "xsrf token invalid" errors.
// /auth/logout-confirm that renders a styled form posting back to // Clicking sign-out in the wiki is already confirmation enough.
// /oidc/session/end/confirm with that xsrf value. The token rides ctx.type = "html";
// in a short-lived httpOnly cookie so it never hits the URL. ctx.status = 200;
const match = form.match(/name="xsrf"\s+value="([^"]+)"/); ctx.body = `<!DOCTYPE html>
if (!match) { <html>
// Defensive: if oidc-provider ever changes its form format, fall <head>
// back to the raw form so logout still works. <title>Signing out Ghost Guild</title>
ctx.type = "html"; <meta name="viewport" content="width=device-width, initial-scale=1" />
ctx.status = 200; <style>
ctx.body = `<!DOCTYPE html><html><body>${form}<script>document.getElementById('op.logoutForm').submit()</script></body></html>`; html, body { margin: 0; padding: 0; height: 100%; background: #1a1814; color: #e8d9b8; font-family: "Commit Mono", ui-monospace, monospace; }
return; body { display: grid; place-items: center; }
} p { font-size: 13px; letter-spacing: 0.05em; text-transform: uppercase; color: #b09c76; }
ctx.cookies.set("oidc_logout_xsrf", match[1], { form { display: none; }
</style>
</head>
<body>
<p>Signing you out</p>
${form}
<script>document.getElementById('op.logoutForm').submit()</script>
</body>
</html>`;
},
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, httpOnly: true,
sameSite: "lax", sameSite: "lax",
maxAge: 120_000, // 2 minutes
path: "/", path: "/",
overwrite: true, overwrite: true,
signed: false, signed: false,
}); });
ctx.redirect("/auth/logout-confirm");
},
postLogoutSuccessSource: async (ctx: any) => {
ctx.redirect("/auth/logout-success"); ctx.redirect("/auth/logout-success");
}, },
}, },