Fix auth flow: federated logout, login page move, and healthcheck

- Add federated logout endpoint that clears Auth.js session AND ends
  Zitadel SSO session via OIDC end_session endpoint
- Move sign-in page from /auth/signin to /login to avoid Auth.js
  route conflict causing ERR_TOO_MANY_REDIRECTS
- Add callbackUrl to all signIn calls so users land on /dashboard
- Store id_token in session for federated logout id_token_hint
- Fix Zitadel healthcheck using binary ready command (no curl needed)
- Update post_logout_redirect_uri in setup script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikkel Georgsen 2026-02-08 12:55:35 +01:00
parent a22ba48709
commit c0cb2d25a0
12 changed files with 54 additions and 15 deletions

View file

@ -3,6 +3,7 @@
declare module '@auth/sveltekit' {
interface Session {
accessToken?: string;
idToken?: string;
}
}
@ -10,6 +11,7 @@ declare module '@auth/core/jwt' {
interface JWT {
accessToken?: string;
refreshToken?: string;
idToken?: string;
expiresAt?: number;
}
}

View file

@ -29,17 +29,19 @@ export const { handle, signIn, signOut } = SvelteKitAuth({
if (account) {
token.accessToken = account.access_token;
token.refreshToken = account.refresh_token;
token.idToken = account.id_token;
token.expiresAt = account.expires_at;
}
return token;
},
async session({ session, token }) {
session.accessToken = token.accessToken as string;
session.idToken = token.idToken as string;
return session;
}
},
pages: {
signIn: '/auth/signin'
signIn: '/login'
},
trustHost: true
});

View file

@ -23,7 +23,7 @@
</a>
{:else}
<button
onclick={() => signIn('zitadel')}
onclick={() => signIn('zitadel', { callbackUrl: '/dashboard' })}
class="px-5 py-2 bg-blue-600 hover:bg-blue-500 rounded-lg font-medium transition-colors cursor-pointer"
>
Sign In
@ -50,7 +50,7 @@
</a>
{:else}
<button
onclick={() => signIn('zitadel')}
onclick={() => signIn('zitadel', { callbackUrl: '/dashboard' })}
class="px-8 py-3 bg-blue-600 hover:bg-blue-500 rounded-lg text-lg font-medium transition-colors cursor-pointer"
>
Get Started

View file

@ -0,0 +1,26 @@
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { env } from '$env/dynamic/private';
export const GET: RequestHandler = async (event) => {
const session = await event.locals.auth();
const idToken = session?.idToken;
// Build Zitadel end_session URL
const issuer = env.AUTH_ZITADEL_ISSUER || 'http://localhost:8080';
const endSessionUrl = new URL('/oidc/v1/end_session', issuer);
if (idToken) {
endSessionUrl.searchParams.set('id_token_hint', idToken);
}
// Use the registered post_logout_redirect_uri
endSessionUrl.searchParams.set('post_logout_redirect_uri', 'http://localhost:5173');
// Clear the Auth.js session cookies
event.cookies.delete('authjs.session-token', { path: '/' });
event.cookies.delete('__Secure-authjs.session-token', { path: '/' });
event.cookies.delete('authjs.callback-url', { path: '/' });
event.cookies.delete('authjs.csrf-token', { path: '/' });
throw redirect(302, endSessionUrl.toString());
};

View file

@ -4,7 +4,7 @@ import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async (event) => {
const session = await event.locals.auth();
if (!session) {
throw redirect(303, '/auth/signin');
throw redirect(303, '/login');
}
return { session };
};

View file

@ -1,7 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { signOut } from '@auth/sveltekit/client';
let { children } = $props();
let session = $derived($page.data.session);
let sidebarOpen = $state(false);
@ -131,7 +129,7 @@
</div>
{/if}
<button
onclick={() => signOut()}
onclick={() => window.location.href = '/api/auth/federated-logout'}
class="text-sm text-slate-500 hover:text-red-600 dark:text-slate-400 dark:hover:text-red-400 transition-colors cursor-pointer"
title="Sign out"
>

View file

@ -1,6 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { signOut } from '@auth/sveltekit/client';
let session = $derived($page.data.session);
const zitadelAccountUrl = import.meta.env.PUBLIC_ZITADEL_ACCOUNT_URL || '#';
@ -91,7 +90,7 @@
Sign out of your PVM account on this device.
</p>
<button
onclick={() => signOut({ callbackUrl: '/' })}
onclick={() => window.location.href = '/api/auth/federated-logout'}
class="inline-flex items-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium transition-colors cursor-pointer"
>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">

View file

@ -0,0 +1,9 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
const session = await event.locals.auth();
if (session) {
throw redirect(303, '/dashboard');
}
};

View file

@ -17,7 +17,7 @@
<div class="bg-white/5 border border-white/10 rounded-2xl p-8 backdrop-blur-sm">
<!-- OIDC Sign In -->
<button
onclick={() => signIn('zitadel')}
onclick={() => signIn('zitadel', { callbackUrl: '/dashboard' })}
class="w-full flex items-center justify-center gap-3 px-4 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition-colors cursor-pointer"
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
@ -37,7 +37,7 @@
<div class="space-y-3">
<button
onclick={() => signIn('zitadel')}
onclick={() => signIn('zitadel', { callbackUrl: '/dashboard' })}
class="w-full flex items-center justify-center gap-3 px-4 py-3 bg-white/5 hover:bg-white/10 border border-white/10 text-white rounded-lg font-medium transition-colors cursor-pointer"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
@ -50,7 +50,7 @@
</button>
<button
onclick={() => signIn('zitadel')}
onclick={() => signIn('zitadel', { callbackUrl: '/dashboard' })}
class="w-full flex items-center justify-center gap-3 px-4 py-3 bg-white/5 hover:bg-white/10 border border-white/10 text-white rounded-lg font-medium transition-colors cursor-pointer"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
@ -60,7 +60,7 @@
</button>
<button
onclick={() => signIn('zitadel')}
onclick={() => signIn('zitadel', { callbackUrl: '/dashboard' })}
class="w-full flex items-center justify-center gap-3 px-4 py-3 bg-white/5 hover:bg-white/10 border border-white/10 text-white rounded-lg font-medium transition-colors cursor-pointer"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="#1877F2">

View file

@ -39,8 +39,9 @@ services:
condition: service_healthy
volumes:
- ./machinekey:/machinekey
- ./zitadel-healthcheck.yaml:/zitadel-healthcheck.yaml:ro
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/debug/healthz"]
test: ["CMD", "/app/zitadel", "ready", "--config", "/zitadel-healthcheck.yaml"]
interval: 10s
timeout: 5s
retries: 15

View file

@ -102,7 +102,7 @@ create_oidc_app() {
-d '{
"name": "PVM Dashboard",
"redirectUris": ["http://localhost:5173/auth/callback/zitadel"],
"postLogoutRedirectUris": ["http://localhost:5173"],
"postLogoutRedirectUris": ["http://localhost:5173", "http://localhost:5173/login"],
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
"grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE"],
"appType": "OIDC_APP_TYPE_WEB",

View file

@ -0,0 +1,2 @@
TLS:
Mode: disabled