fix(auth): rewire OIDC logout/error flow through Nuxt pages
Some checks failed
Test / playwright (push) Blocked by required conditions
Test / Notify on failure (push) Blocked by required conditions
Test / visual (push) Blocked by required conditions
Test / vitest (push) Has been cancelled

Migrate three render callbacks in oidc-provider (logoutSource,
postLogoutSuccessSource, renderError) from the baked guildPageShell
helper to Nuxt pages under app/pages/auth/, so they go through the
font module and design system instead of a shadow copy.

- Delete guildPageShell (~103 lines of shadow design system).
- Add /auth/logout-success, /auth/oidc-error, /auth/logout-confirm
  pages built on dashed-box + btn + main.css tokens.
- renderError now allow-lists error + error_description into query
  params and lets Vue default interpolation escape them, closing an
  XSS where OIDC error fields were concatenated into raw HTML.
- logoutSource extracts the xsrf from oidc-provider's stable form
  output, sets it as an httpOnly 2-minute cookie, and redirects to
  /auth/logout-confirm. The confirm page reads the cookie during SSR,
  persists the value to useState, and clears the cookie so it's
  strictly one-time use. Defensive fallback keeps the raw auto-submit
  form if oidc-provider ever changes its form format.
- Fix form actions emitting http:// in production at the root cause:
  oidc-provider extends Koa but calls super() with no args, so
  app.proxy defaults to false and ctx.protocol ignores
  X-Forwarded-Proto. Set _provider.proxy = true after construction;
  remove the bogus proxy:true config key (silently ignored) and the
  form.replace('http://', 'https://') symptom patch. Make the
  x-forwarded-proto override in the catchall conditional on
  production + missing header (was unconditional + dead code).
- Add site-wide .btn:focus-visible rule in main.css for WCAG 2.4.7.

Verified in browser: Brygada 1918 loads on all three pages, contrast
ratios pass AA in dark + light, XSS payload escapes to text nodes
only, Set-Cookie: Max-Age=0 enforces one-time xsrf use, no
horizontal overflow at 500px, no console errors.
This commit is contained in:
Jennie Robinson Faber 2026-04-11 23:21:46 +01:00
parent 98d3610a08
commit de3bcc479a
6 changed files with 367 additions and 143 deletions

View file

@ -0,0 +1,115 @@
<script setup lang="ts">
definePageMeta({ layout: false });
useHead({ title: "Sign-In Error — Ghost Guild" });
const route = useRoute();
// Vue's default {{ }} interpolation escapes HTML on render, so these
// values from the query string can never execute as markup fixing the
// XSS that existed in the old guildPageShell renderError implementation.
const errorCode = computed(() =>
typeof route.query.error === "string" ? route.query.error : "",
);
const errorDescription = computed(() =>
typeof route.query.error_description === "string"
? route.query.error_description
: "",
);
const hasDetail = computed(
() => Boolean(errorCode.value) || Boolean(errorDescription.value),
);
</script>
<template>
<main class="auth-shell">
<div class="dashed-box auth-box">
<header class="auth-header">
<p class="section-label">Ghost Guild</p>
<h1 class="auth-title">Something went wrong</h1>
</header>
<hr class="section-divider" />
<p class="auth-body">
An error occurred during authentication. Please try again.
</p>
<div v-if="hasDetail" class="auth-detail" role="status">
<p v-if="errorCode" class="auth-detail-code">{{ errorCode }}</p>
<p v-if="errorDescription" class="auth-detail-desc">
{{ errorDescription }}
</p>
</div>
<a href="https://wiki.ghostguild.org" class="btn btn-primary auth-btn">
Return to Wiki
</a>
</div>
</main>
</template>
<style scoped>
.auth-shell {
display: grid;
place-items: center;
min-height: 100vh;
min-height: 100dvh;
padding: var(--page-pad-y) var(--page-pad-x);
}
.auth-box {
width: 100%;
max-width: 420px;
padding: 24px 28px;
}
.auth-header {
text-align: center;
}
.auth-title {
font-family: var(--font-display);
font-size: 28px;
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.01em;
color: var(--candle);
margin: 0;
}
.auth-body {
font-size: 14px;
color: var(--text);
line-height: 1.55;
text-align: center;
margin: 0;
}
.auth-detail {
border: 1px dashed var(--border);
padding: 12px 14px;
font-family: "Commit Mono", monospace;
font-size: 12px;
color: var(--text-dim);
text-align: left;
word-break: break-word;
}
.auth-detail-code {
color: var(--ember);
font-weight: 700;
margin: 0 0 4px;
}
.auth-detail-desc {
margin: 0;
}
.auth-btn {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
margin-top: 4px;
}
</style>