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:
parent
a22ba48709
commit
c0cb2d25a0
12 changed files with 54 additions and 15 deletions
2
apps/dashboard/src/app.d.ts
vendored
2
apps/dashboard/src/app.d.ts
vendored
|
|
@ -3,6 +3,7 @@
|
||||||
declare module '@auth/sveltekit' {
|
declare module '@auth/sveltekit' {
|
||||||
interface Session {
|
interface Session {
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
idToken?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10,6 +11,7 @@ declare module '@auth/core/jwt' {
|
||||||
interface JWT {
|
interface JWT {
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
|
idToken?: string;
|
||||||
expiresAt?: number;
|
expiresAt?: number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,17 +29,19 @@ export const { handle, signIn, signOut } = SvelteKitAuth({
|
||||||
if (account) {
|
if (account) {
|
||||||
token.accessToken = account.access_token;
|
token.accessToken = account.access_token;
|
||||||
token.refreshToken = account.refresh_token;
|
token.refreshToken = account.refresh_token;
|
||||||
|
token.idToken = account.id_token;
|
||||||
token.expiresAt = account.expires_at;
|
token.expiresAt = account.expires_at;
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
session.accessToken = token.accessToken as string;
|
session.accessToken = token.accessToken as string;
|
||||||
|
session.idToken = token.idToken as string;
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
signIn: '/auth/signin'
|
signIn: '/login'
|
||||||
},
|
},
|
||||||
trustHost: true
|
trustHost: true
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<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"
|
class="px-5 py-2 bg-blue-600 hover:bg-blue-500 rounded-lg font-medium transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
Sign In
|
Sign In
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<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"
|
class="px-8 py-3 bg-blue-600 hover:bg-blue-500 rounded-lg text-lg font-medium transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
};
|
||||||
|
|
@ -4,7 +4,7 @@ import type { LayoutServerLoad } from './$types';
|
||||||
export const load: LayoutServerLoad = async (event) => {
|
export const load: LayoutServerLoad = async (event) => {
|
||||||
const session = await event.locals.auth();
|
const session = await event.locals.auth();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw redirect(303, '/auth/signin');
|
throw redirect(303, '/login');
|
||||||
}
|
}
|
||||||
return { session };
|
return { session };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { signOut } from '@auth/sveltekit/client';
|
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
let session = $derived($page.data.session);
|
let session = $derived($page.data.session);
|
||||||
let sidebarOpen = $state(false);
|
let sidebarOpen = $state(false);
|
||||||
|
|
@ -131,7 +129,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<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"
|
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"
|
title="Sign out"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { signOut } from '@auth/sveltekit/client';
|
|
||||||
let session = $derived($page.data.session);
|
let session = $derived($page.data.session);
|
||||||
|
|
||||||
const zitadelAccountUrl = import.meta.env.PUBLIC_ZITADEL_ACCOUNT_URL || '#';
|
const zitadelAccountUrl = import.meta.env.PUBLIC_ZITADEL_ACCOUNT_URL || '#';
|
||||||
|
|
@ -91,7 +90,7 @@
|
||||||
Sign out of your PVM account on this device.
|
Sign out of your PVM account on this device.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<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"
|
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">
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
|
|
||||||
9
apps/dashboard/src/routes/login/+page.server.ts
Normal file
9
apps/dashboard/src/routes/login/+page.server.ts
Normal 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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
<div class="bg-white/5 border border-white/10 rounded-2xl p-8 backdrop-blur-sm">
|
<div class="bg-white/5 border border-white/10 rounded-2xl p-8 backdrop-blur-sm">
|
||||||
<!-- OIDC Sign In -->
|
<!-- OIDC Sign In -->
|
||||||
<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-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition-colors cursor-pointer"
|
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">
|
<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">
|
<div class="space-y-3">
|
||||||
<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"
|
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">
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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"
|
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">
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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"
|
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">
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="#1877F2">
|
||||||
|
|
@ -39,8 +39,9 @@ services:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
volumes:
|
volumes:
|
||||||
- ./machinekey:/machinekey
|
- ./machinekey:/machinekey
|
||||||
|
- ./zitadel-healthcheck.yaml:/zitadel-healthcheck.yaml:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-sf", "http://localhost:8080/debug/healthz"]
|
test: ["CMD", "/app/zitadel", "ready", "--config", "/zitadel-healthcheck.yaml"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 15
|
retries: 15
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ create_oidc_app() {
|
||||||
-d '{
|
-d '{
|
||||||
"name": "PVM Dashboard",
|
"name": "PVM Dashboard",
|
||||||
"redirectUris": ["http://localhost:5173/auth/callback/zitadel"],
|
"redirectUris": ["http://localhost:5173/auth/callback/zitadel"],
|
||||||
"postLogoutRedirectUris": ["http://localhost:5173"],
|
"postLogoutRedirectUris": ["http://localhost:5173", "http://localhost:5173/login"],
|
||||||
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
|
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
|
||||||
"grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE"],
|
"grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE"],
|
||||||
"appType": "OIDC_APP_TYPE_WEB",
|
"appType": "OIDC_APP_TYPE_WEB",
|
||||||
|
|
|
||||||
2
docker/zitadel-healthcheck.yaml
Normal file
2
docker/zitadel-healthcheck.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
TLS:
|
||||||
|
Mode: disabled
|
||||||
Loading…
Add table
Reference in a new issue