From 3b745836bd520b06020fb58a94b3a363f2363772 Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 8 May 2026 21:52:46 +0200 Subject: [PATCH] =?UTF-8?q?fix(auth):=20two-step=20Login=20(Better-Auth-na?= =?UTF-8?q?tive=20+=20/refresh)=20f=C3=BCr=20SSO-Cookie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mana-auth's /api/v1/auth/login schluckt das Set-Cookie der signedSession (es nutzt die Cookie nur intern um den JWT zu minten) — der Browser bekommt nur einen JSON-Body ohne Cookie. Damit funktioniert der spätere /refresh-Endpoint, der auf Cookie-Auth basiert, nicht. Two-step-Login: 1. POST /api/auth/sign-in/email (Better-Auth-native) — setzt das __Secure-mana.session_token Cookie auf Domain=.mana.how. Liefert User-Profil im Body. 2. POST /api/v1/auth/refresh mit credentials:include → accessToken. Effekt: spätere Auto-Refreshs, Cross-App-SSO und 401-Retries laufen korrekt. Memo für die Plattform-Seite: /api/v1/auth/login könnte das Set-Cookie weiterreichen — dann wäre der zweite Call überflüssig. Heute: aktiver Workaround. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/src/lib/auth/dev-stub.svelte.ts | 45 ++++++++++++++++-------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/web/src/lib/auth/dev-stub.svelte.ts b/apps/web/src/lib/auth/dev-stub.svelte.ts index 96fa783..6df061b 100644 --- a/apps/web/src/lib/auth/dev-stub.svelte.ts +++ b/apps/web/src/lib/auth/dev-stub.svelte.ts @@ -99,34 +99,49 @@ class Session { return this.user?.id ?? this.stubId ?? null; } - /** Login gegen mana-auth, speichert accessToken + Profil. */ + /** + * Login gegen mana-auth — zwei Calls in einem Flow: + * + * 1. Better-Auth-Native `POST /api/auth/sign-in/email` setzt das + * SSO-Cookie (__Secure-mana.session_token) auf Domain `.mana.how` + * und liefert das User-Profil. Dieser Endpoint erzeugt im + * Gegensatz zu `/api/v1/auth/login` ein gültiges Set-Cookie auf + * der Response. + * 2. `POST /api/v1/auth/refresh` mit Cookie-Auth liefert den + * EdDSA-JWT-accessToken aus der frischen Session. + * + * Damit funktioniert auch der spätere Background-Refresh (gleicher + * Endpoint) — und User-Sessions überleben einen Tab-Close, wenn das + * localStorage geleert wurde aber das HttpOnly-Cookie noch lebt. + */ async login(email: string, password: string): Promise { - const r = await fetch(`${authBaseUrl()}/api/v1/auth/login`, { + const signIn = await fetch(`${authBaseUrl()}/api/auth/sign-in/email`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, password }), }); - if (!r.ok) { - const body = await r.text().catch(() => ''); - throw new Error(`Login fehlgeschlagen (${r.status}): ${body.slice(0, 120)}`); + if (!signIn.ok) { + const body = await signIn.text().catch(() => ''); + throw new Error(`Login fehlgeschlagen (${signIn.status}): ${body.slice(0, 120)}`); } - const data = (await r.json()) as { - accessToken: string; + const signInData = (await signIn.json()) as { user: { id: string; email: string; name?: string; accessTier?: string }; }; - const claims = decodeJwt(data.accessToken); - if (!claims) throw new Error('Auth-Server lieferte ein ungültiges Token zurück.'); - this.token = data.accessToken; + // JWT aus der Cookie-Session ziehen. + const refreshOk = await this.tryRefresh(); + if (!refreshOk || !this.token) { + throw new Error('Auth-Server lieferte kein gültiges Token nach Login.'); + } + this.user = { - id: data.user.id, - email: data.user.email, - name: data.user.name ?? null, - tier: data.user.accessTier ?? 'public', + id: signInData.user.id, + email: signInData.user.email, + name: signInData.user.name ?? null, + tier: signInData.user.accessTier ?? 'public', }; if (typeof window !== 'undefined') { - window.localStorage.setItem(TOKEN_KEY, data.accessToken); window.localStorage.setItem(USER_KEY, JSON.stringify(this.user)); } }