2025-07-16 19:43:18 +02:00

176 lines
4.8 KiB
TypeScript

import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
import { serialize, parse } from 'cookie';
import { config } from 'dotenv';
// Load environment variables
config();
// Environment variables - use runtime access for server-side
function getEnv(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing environment variable: ${key}`);
}
return value;
}
const SECRET_KEY = new TextEncoder().encode(getEnv('OIDC_CLIENT_SECRET'));
const SESSION_DURATION = 6 * 60 * 60; // 6 hours in seconds
export interface SessionData {
userId: string;
authenticated: boolean;
exp: number;
}
// Create a signed JWT session token
export async function createSession(userId: string): Promise<string> {
const exp = Math.floor(Date.now() / 1000) + SESSION_DURATION;
return await new SignJWT({
userId,
authenticated: true,
exp
})
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime(exp)
.sign(SECRET_KEY);
}
// Verify and decode a session token
export async function verifySession(token: string): Promise<SessionData | null> {
try {
const { payload } = await jwtVerify(token, SECRET_KEY);
// Validate payload structure and cast properly
if (
typeof payload.userId === 'string' &&
typeof payload.authenticated === 'boolean' &&
typeof payload.exp === 'number'
) {
return {
userId: payload.userId,
authenticated: payload.authenticated,
exp: payload.exp
};
}
return null;
} catch (error) {
console.log('Session verification failed:', error);
return null;
}
}
// Get session from request cookies
export function getSessionFromRequest(request: Request): string | null {
const cookieHeader = request.headers.get('cookie');
if (!cookieHeader) return null;
const cookies = parse(cookieHeader);
return cookies.session || null;
}
// Create session cookie
export function createSessionCookie(token: string): string {
const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
const isSecure = publicBaseUrl.startsWith('https://');
return serialize('session', token, {
httpOnly: true,
secure: isSecure,
sameSite: 'lax',
maxAge: SESSION_DURATION,
path: '/'
});
}
// Clear session cookie
export function clearSessionCookie(): string {
const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
const isSecure = publicBaseUrl.startsWith('https://');
return serialize('session', '', {
httpOnly: true,
secure: isSecure,
sameSite: 'lax',
maxAge: 0,
path: '/'
});
}
// Generate OIDC authorization URL
export function generateAuthUrl(state: string): string {
const oidcEndpoint = getEnv('OIDC_ENDPOINT');
const clientId = getEnv('OIDC_CLIENT_ID');
const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
const params = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: `${publicBaseUrl}/auth/callback`,
scope: 'openid profile email',
state: state
});
return `${oidcEndpoint}/apps/oidc/authorize?${params.toString()}`;
}
// Exchange authorization code for tokens
export async function exchangeCodeForTokens(code: string): Promise<any> {
const oidcEndpoint = getEnv('OIDC_ENDPOINT');
const clientId = getEnv('OIDC_CLIENT_ID');
const clientSecret = getEnv('OIDC_CLIENT_SECRET');
const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
const response = await fetch(`${oidcEndpoint}/apps/oidc/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: `${publicBaseUrl}/auth/callback`
})
});
if (!response.ok) {
const error = await response.text();
console.error('Token exchange failed:', error);
throw new Error('Failed to exchange authorization code');
}
return await response.json();
}
// Get user info from OIDC provider
export async function getUserInfo(accessToken: string): Promise<any> {
const oidcEndpoint = getEnv('OIDC_ENDPOINT');
const response = await fetch(`${oidcEndpoint}/apps/oidc/userinfo`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
const error = await response.text();
console.error('Userinfo request failed:', error);
throw new Error('Failed to get user info');
}
return await response.json();
}
// Generate random state for CSRF protection
export function generateState(): string {
return crypto.randomUUID();
}
// Log authentication events for debugging
export function logAuthEvent(event: string, details?: any) {
const timestamp = new Date().toISOString();
console.log(`[AUTH ${timestamp}] ${event}`, details ? JSON.stringify(details) : '');
}