Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
6 changed files with 69 additions and 32 deletions
Showing only changes of commit 539fad1d22 - Show all commits
+7 -1
View File
@@ -7,8 +7,14 @@ ZITADEL_CLIENT_SECRET=your-web-app-client-secret
AUTH_REDIRECT_URI=http://localhost:5173/auth/callback AUTH_REDIRECT_URI=http://localhost:5173/auth/callback
AUTH_POST_LOGOUT_URI=http://localhost:5173/ AUTH_POST_LOGOUT_URI=http://localhost:5173/
# Secret used to sign the session cookie (any long random string) # Secret used to derive the session cookie encryption key (any long random string)
SESSION_SECRET=change-me-to-a-long-random-string SESSION_SECRET=change-me-to-a-long-random-string
# Optional session tuning (defaults shown)
# OIDC_SCOPE=openid profile email offline_access
# SESSION_TTL_SECONDS=604800
# SESSION_REFRESH_THRESHOLD_SECONDS=30
# AUTH_FLOW_TTL_SECONDS=600
# Backend API base URL (used from later parts) # Backend API base URL (used from later parts)
API_BASE_URL=http://localhost:5111 API_BASE_URL=http://localhost:5111
+2 -3
View File
@@ -6,13 +6,12 @@ import {
sessionCookieOptions, sessionCookieOptions,
SESSION_COOKIE SESSION_COOKIE
} from '$lib/server/auth'; } from '$lib/server/auth';
import { authConfig } from '$lib/server/config';
const REFRESH_THRESHOLD_SECONDS = 30;
export const handle: Handle = async ({ event, resolve }) => { export const handle: Handle = async ({ event, resolve }) => {
let session = await decodeSession(event.cookies.get(SESSION_COOKIE)); let session = await decodeSession(event.cookies.get(SESSION_COOKIE));
if (session && session.expiresAt - REFRESH_THRESHOLD_SECONDS <= Math.floor(Date.now() / 1000)) { if (session && session.expiresAt - authConfig.refreshThresholdSeconds <= Math.floor(Date.now() / 1000)) {
session = await refreshSession(session); session = await refreshSession(session);
if (session) { if (session) {
event.cookies.set(SESSION_COOKIE, await encodeSession(session), sessionCookieOptions(event.url)); event.cookies.set(SESSION_COOKIE, await encodeSession(session), sessionCookieOptions(event.url));
+11 -23
View File
@@ -1,6 +1,6 @@
import * as client from 'openid-client'; import * as client from 'openid-client';
import { EncryptJWT, jwtDecrypt } from 'jose'; import { EncryptJWT, jwtDecrypt } from 'jose';
import { env } from '$env/dynamic/private'; import { authConfig } from '$lib/server/config';
import type { SessionUser } from '$lib/types'; import type { SessionUser } from '$lib/types';
export const SESSION_COOKIE = 'mf_session'; export const SESSION_COOKIE = 'mf_session';
@@ -16,40 +16,27 @@ export interface Session extends SessionUser {
expiresAt: number; expiresAt: number;
} }
export const OIDC_SCOPE = 'openid profile email offline_access';
let configPromise: Promise<client.Configuration> | null = null; let configPromise: Promise<client.Configuration> | null = null;
export function getOidcConfig(): Promise<client.Configuration> { export function getOidcConfig(): Promise<client.Configuration> {
configPromise ??= (async () => { configPromise ??= (async () => {
const issuer = new URL(env.ZITADEL_ISSUER); const issuer = new URL(authConfig.issuer);
const options = const options =
issuer.protocol === 'http:' ? { execute: [client.allowInsecureRequests] } : undefined; issuer.protocol === 'http:' ? { execute: [client.allowInsecureRequests] } : undefined;
const secret = env.ZITADEL_CLIENT_SECRET; const secret = authConfig.clientSecret;
const hasSecret = !!secret && !secret.startsWith('REPLACE_'); return secret
? client.discovery(issuer, authConfig.clientId, secret, undefined, options)
return hasSecret : client.discovery(issuer, authConfig.clientId, undefined, client.None(), options);
? client.discovery(issuer, env.ZITADEL_CLIENT_ID, secret, undefined, options)
: client.discovery(issuer, env.ZITADEL_CLIENT_ID, undefined, client.None(), options);
})(); })();
return configPromise; return configPromise;
} }
export const authConfig = {
get redirectUri() {
return env.AUTH_REDIRECT_URI;
},
get postLogoutUri() {
return env.AUTH_POST_LOGOUT_URI;
}
};
let keyPromise: Promise<Uint8Array> | null = null; let keyPromise: Promise<Uint8Array> | null = null;
function sessionKey(): Promise<Uint8Array> { function sessionKey(): Promise<Uint8Array> {
keyPromise ??= crypto.subtle keyPromise ??= crypto.subtle
.digest('SHA-256', new TextEncoder().encode(env.SESSION_SECRET)) .digest('SHA-256', new TextEncoder().encode(authConfig.sessionSecret))
.then((digest) => new Uint8Array(digest)); .then((digest) => new Uint8Array(digest));
return keyPromise; return keyPromise;
} }
@@ -60,15 +47,16 @@ export function sessionCookieOptions(url: URL) {
sameSite: 'lax' as const, sameSite: 'lax' as const,
secure: url.protocol === 'https:', secure: url.protocol === 'https:',
path: '/', path: '/',
maxAge: 60 * 60 * 24 * 7 maxAge: authConfig.sessionTtlSeconds
}; };
} }
export async function encodeSession(session: Session): Promise<string> { export async function encodeSession(session: Session): Promise<string> {
const now = Math.floor(Date.now() / 1000);
return new EncryptJWT({ ...session }) return new EncryptJWT({ ...session })
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setIssuedAt() .setIssuedAt(now)
.setExpirationTime('7d') .setExpirationTime(now + authConfig.sessionTtlSeconds)
.encrypt(await sessionKey()); .encrypt(await sessionKey());
} }
+44
View File
@@ -0,0 +1,44 @@
import { env } from '$env/dynamic/private';
const DAY_SECONDS = 60 * 60 * 24;
const DEFAULT_OIDC_SCOPE = 'openid profile email offline_access';
function positiveInt(value: string | undefined, fallback: number): number {
if (value === undefined || value === '') return fallback;
const parsed = Number(value);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
export const authConfig = {
get issuer() {
return env.ZITADEL_ISSUER;
},
get clientId() {
return env.ZITADEL_CLIENT_ID;
},
get clientSecret(): string | undefined {
const secret = env.ZITADEL_CLIENT_SECRET;
return secret && !secret.startsWith('REPLACE_') ? secret : undefined;
},
get scope() {
return env.OIDC_SCOPE || DEFAULT_OIDC_SCOPE;
},
get redirectUri() {
return env.AUTH_REDIRECT_URI;
},
get postLogoutUri() {
return env.AUTH_POST_LOGOUT_URI;
},
get sessionSecret() {
return env.SESSION_SECRET;
},
get sessionTtlSeconds() {
return positiveInt(env.SESSION_TTL_SECONDS, 7 * DAY_SECONDS);
},
get refreshThresholdSeconds() {
return positiveInt(env.SESSION_REFRESH_THRESHOLD_SECONDS, 30);
},
get flowTtlSeconds() {
return positiveInt(env.AUTH_FLOW_TTL_SECONDS, 600);
}
};
+3 -4
View File
@@ -3,13 +3,12 @@ import * as client from 'openid-client';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
import { import {
getOidcConfig, getOidcConfig,
authConfig,
OIDC_SCOPE,
STATE_COOKIE, STATE_COOKIE,
VERIFIER_COOKIE, VERIFIER_COOKIE,
NONCE_COOKIE, NONCE_COOKIE,
RETURN_COOKIE RETURN_COOKIE
} from '$lib/server/auth'; } from '$lib/server/auth';
import { authConfig } from '$lib/server/config';
export const GET: RequestHandler = async ({ url, cookies }) => { export const GET: RequestHandler = async ({ url, cookies }) => {
const config = await getOidcConfig(); const config = await getOidcConfig();
@@ -24,7 +23,7 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
const parameters: Record<string, string> = { const parameters: Record<string, string> = {
redirect_uri: authConfig.redirectUri, redirect_uri: authConfig.redirectUri,
scope: OIDC_SCOPE, scope: authConfig.scope,
code_challenge: codeChallenge, code_challenge: codeChallenge,
code_challenge_method: 'S256', code_challenge_method: 'S256',
state, state,
@@ -39,7 +38,7 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
sameSite: 'lax' as const, sameSite: 'lax' as const,
secure: url.protocol === 'https:', secure: url.protocol === 'https:',
path: '/', path: '/',
maxAge: 600 maxAge: authConfig.flowTtlSeconds
}; };
cookies.set(VERIFIER_COOKIE, codeVerifier, cookieOptions); cookies.set(VERIFIER_COOKIE, codeVerifier, cookieOptions);
cookies.set(STATE_COOKIE, state, cookieOptions); cookies.set(STATE_COOKIE, state, cookieOptions);
+2 -1
View File
@@ -1,7 +1,8 @@
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import * as client from 'openid-client'; import * as client from 'openid-client';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
import { getOidcConfig, authConfig, decodeSession, SESSION_COOKIE } from '$lib/server/auth'; import { getOidcConfig, decodeSession, SESSION_COOKIE } from '$lib/server/auth';
import { authConfig } from '$lib/server/config';
const handleLogout: RequestHandler = async ({ cookies }) => { const handleLogout: RequestHandler = async ({ cookies }) => {
const session = await decodeSession(cookies.get(SESSION_COOKIE)); const session = await decodeSession(cookies.get(SESSION_COOKIE));