Operator UI for the Authentication Service: passwordless email magic-link sign up / sign in / sign out.
SvelteKit 2 · Svelte 5 (runes) · TypeScript strict · BFF (server-side session) · bilingual (English + Welsh / Cymraeg).
Session model. Login establishes a server-side session; the browser holds only the httpOnly
__Host-mxi_sessioncookie — no token in JS. See the canonical design docAGENTS/share/authentication-sessions.md. This supersedes the prior bearer-token /localStorageSPA model (and its#access_token=cross-origin handoff).
| Route | Purpose |
|---|---|
/ |
Account dashboard (current user, sign out) — server load reads the session cookie |
/signup |
Create an account → emailed a magic link |
/signin |
Request a magic link for an existing account |
/verify?token=… |
Consume the magic link server-side → session cookie set → redirect home |
/admin/attributes |
ABAC attribute-assignment admin UI (?pid=…) — view / replace a user's attributes; gated on an access=admin caller |
- Node 20+ and pnpm
- A running Authentication Service
cp .env.example .env # PUBLIC_API_BASE_URL=http://localhost:5150
pnpm install
pnpm dev # http://localhost:5173Sign up, then look at the auth service console — in development the
magic link is logged there (no SMTP). Open it to land on /verify, which
exchanges the token server-side; the auth service sets the
__Host-mxi_session cookie and you are signed in.
The UI is bilingual: English (en) and Welsh / Cymraeg (cy) —
the latter a deliberate UK public-sector Welsh-language-duty choice. Pick
a language via the Lily LocaleSelect in the top-bar layout (a Lily
ThemeSelect sits beside it for theme choice); the locale persists to
localStorage["mxi.auth.locale"] and re-renders every string live. It is
also sent as a locale hint on sign-up / sign-in so the magic-link
email arrives in the same language. There is no i18n library — just a
small per-locale catalog and a reactive store in
src/lib/i18n.svelte.ts.
| Var | Default | Purpose |
|---|---|---|
PUBLIC_API_BASE_URL |
http://localhost:5150 |
Auth service REST base URL (no trailing slash). Called server-side by the BFF. |
VITE_RETURN_TO_ALLOWLIST |
(empty) | Comma-separated operator-app origins (exact scheme://host[:port]) the post-verify redirect may target. Unset/empty ⇒ same-origin only. An open-redirect control — no credential travels in the redirect. |
This front-end is a Backend-For-Frontend (BFF): its SvelteKit
server (hooks.server.ts / +page.server.ts / +server.ts) is the
only party that talks to the auth service, and the session lives in an
httpOnly cookie the browser cannot read.
- Verifying a magic link is handled server-side; the auth service
establishes a server-side session and returns
Set-Cookie: __Host-mxi_session=…(HttpOnly · Secure ·SameSite=Lax·__Host-prefix), which the BFF relays to the browser. - The dashboard load and sign-out run on the server, reading the cookie
and calling
GET /me/POST /signout(the latter revokes the session and clears the cookie). - No token in browser JS. The
localStorageaccess token,mxi_access_tokenfederation key, andAuthorization: Bearerfrom the browser are all gone. - CSRF: browser→BFF mutating requests carry a per-session CSRF token, validated server-side.
Full design (session table, cookie attributes, CSRF, cross-service PASETO,
rollout): AGENTS/share/authentication-sessions.md.
An operator app on a different origin links here to sign in and is sent back afterwards — but no credential is handed off (each origin is signed in via its own session cookie):
- The operator app links here as
/signin?return_to=<absolute operator-app URL>(or/signup?...). - If
origin(return_to)is allowlisted (VITE_RETURN_TO_ALLOWLIST, or our own origin), it is preserved across the magic-link email round-trip. A non-allowlisted value is ignored. - After
/verifysigns the user in, the browser is redirected toreturn_to— a plain navigation, no#access_token=fragment.
The allowlist (src/lib/auth/return-to.ts) is the open-redirect control.
pnpm run check # svelte-check (strict, 0 errors expected)
pnpm run build
pnpm run test # vitest (unit)
pnpm run test:e2e # playwrightSee AGENTS.md for the src/ tree and conventions, and
spec/index.md for the specification.