refactor(frontend): centralize configurable auth/session values in config.ts
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user