// src/utils/auth.js (FIXED - Cleaned up and enhanced debugging) import type { AstroGlobal } from 'astro'; import crypto from 'crypto'; import { config } from 'dotenv'; import { SignJWT, jwtVerify, type JWTPayload } from 'jose'; import { serialize, parse as parseCookie } from 'cookie'; config(); const SECRET_KEY = new TextEncoder().encode( process.env.AUTH_SECRET || 'forensic-pathways-default-secret-key-change-in-production' ); const SESSION_DURATION = 6 * 60 * 60; // 6 hours in seconds export interface SessionData { userId: string; email: string; authenticated: boolean; exp: number; } export interface AuthContext { authenticated: boolean; session: SessionData | null; userEmail: string; userId: string; } export type AuthContextType = 'contributions' | 'ai' | 'general'; export interface UserInfo { sub?: string; preferred_username?: string; email?: string; given_name?: string; family_name?: string; } export interface AuthStateData { state: string; returnTo: string; } function getEnv(key: string): string { const value = process.env[key]; if (!value) { console.warn(`[AUTH] Missing environment variable: ${key}`); return ''; } return value; } function isAnyAuthEnabled(): boolean { return getAuthRequirement('general') || getAuthRequirement('contributions') || getAuthRequirement('ai'); } export function getSessionFromRequest(request: Request): string | null { const cookieHeader = request.headers.get('cookie'); console.log('[DEBUG] Cookie header:', cookieHeader ? 'present' : 'missing'); if (!cookieHeader) return null; const cookies = parseCookie(cookieHeader); console.log('[DEBUG] Parsed cookies:', Object.keys(cookies)); console.log('[DEBUG] Session cookie found:', !!cookies.session); return cookies.session || null; } export async function verifySession(sessionToken: string): Promise { try { console.log('[DEBUG] Verifying session token, length:', sessionToken.length); const { payload } = await jwtVerify(sessionToken, SECRET_KEY); console.log('[DEBUG] JWT verification successful, payload keys:', Object.keys(payload)); if ( typeof payload.userId === 'string' && typeof payload.email === 'string' && typeof payload.authenticated === 'boolean' && typeof payload.exp === 'number' ) { console.log('[DEBUG] Session validation successful for user:', payload.userId); return { userId: payload.userId, email: payload.email, authenticated: payload.authenticated, exp: payload.exp }; } console.log('[DEBUG] Session payload validation failed, payload:', payload); return null; } catch (error) { console.log('[DEBUG] Session verification failed:', error.message); return null; } } export async function createSession(userId: string, email: string): Promise { const exp = Math.floor(Date.now() / 1000) + SESSION_DURATION; console.log('[DEBUG] Creating session for user:', userId, 'exp:', exp); const token = await new SignJWT({ userId, email, authenticated: true, exp }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime(exp) .sign(SECRET_KEY); console.log('[DEBUG] Session token created, length:', token.length); return token; } export function createSessionCookie(sessionToken: string): string { const publicBaseUrl = getEnv('PUBLIC_BASE_URL'); const isProduction = process.env.NODE_ENV === 'production'; const isSecure = publicBaseUrl.startsWith('https://') || isProduction; const cookie = serialize('session', sessionToken, { httpOnly: true, secure: isSecure, sameSite: 'lax', maxAge: SESSION_DURATION, path: '/' }); console.log('[DEBUG] Created session cookie:', cookie.substring(0, 100) + '...'); return cookie; } export function getUserEmail(userInfo: UserInfo): string { return userInfo.email || userInfo.preferred_username || 'unknown@example.com'; } export function logAuthEvent(event: string, details?: any): void { const timestamp = new Date().toISOString(); console.log(`[AUTH ${timestamp}] ${event}`, details ? JSON.stringify(details) : ''); } export function generateState(): string { return crypto.randomUUID(); } export function generateAuthUrl(state: string): string { if (!isAnyAuthEnabled()) { throw new Error('Authentication is disabled'); } const oidcEndpoint = getEnv('OIDC_ENDPOINT'); const clientId = getEnv('OIDC_CLIENT_ID'); const publicBaseUrl = getEnv('PUBLIC_BASE_URL') || 'http://localhost:4321'; if (!oidcEndpoint || !clientId) { throw new Error('OIDC configuration incomplete'); } const params = new URLSearchParams({ response_type: 'code', client_id: clientId, redirect_uri: `${publicBaseUrl}/auth/callback`, scope: 'openid profile email', state: state }); return `${oidcEndpoint}/authorize?${params.toString()}`; } export async function exchangeCodeForTokens(code: string): Promise<{ access_token: string }> { if (!isAnyAuthEnabled()) { throw new Error('Authentication is disabled'); } const oidcEndpoint = getEnv('OIDC_ENDPOINT'); const clientId = getEnv('OIDC_CLIENT_ID'); const clientSecret = getEnv('OIDC_CLIENT_SECRET'); const publicBaseUrl = getEnv('PUBLIC_BASE_URL') || 'http://localhost:4321'; if (!oidcEndpoint || !clientId || !clientSecret) { throw new Error('OIDC configuration incomplete'); } const response = await fetch(`${oidcEndpoint}/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` }, 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(); } export async function getUserInfo(accessToken: string): Promise { if (!isAnyAuthEnabled()) { throw new Error('Authentication is disabled'); } const oidcEndpoint = getEnv('OIDC_ENDPOINT'); if (!oidcEndpoint) { throw new Error('OIDC configuration incomplete'); } const response = await fetch(`${oidcEndpoint}/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(); } export function parseAuthState(request: Request): { isValid: boolean; stateData: AuthStateData | null; error?: string } { try { const cookieHeader = request.headers.get('cookie'); const cookies = cookieHeader ? parseCookie(cookieHeader) : {}; if (!cookies.auth_state) { return { isValid: false, stateData: null, error: 'No auth state cookie' }; } const stateData = JSON.parse(decodeURIComponent(cookies.auth_state)); if (!stateData.state || !stateData.returnTo) { return { isValid: false, stateData: null, error: 'Invalid state data structure' }; } return { isValid: true, stateData }; } catch (error) { return { isValid: false, stateData: null, error: 'Failed to parse auth state' }; } } export function verifyAuthState(request: Request, receivedState: string): { isValid: boolean; stateData: AuthStateData | null; error?: string; } { const { isValid, stateData, error } = parseAuthState(request); if (!isValid || !stateData) { logAuthEvent('State parsing failed', { error }); return { isValid: false, stateData: null, error }; } if (stateData.state !== receivedState) { logAuthEvent('State mismatch', { received: receivedState, stored: stateData.state }); return { isValid: false, stateData: null, error: 'State parameter mismatch' }; } return { isValid: true, stateData }; } function getAuthRequirement(context: AuthContextType): boolean { switch (context) { case 'contributions': return process.env.AUTHENTICATION_NECESSARY_CONTRIBUTIONS !== 'false'; case 'ai': return process.env.AUTHENTICATION_NECESSARY_AI !== 'false'; case 'general': return process.env.AUTHENTICATION_NECESSARY !== 'false'; default: return true; } } export async function createSessionWithCookie(userInfo: UserInfo): Promise<{ sessionToken: string; sessionCookie: string; clearStateCookie: string; userId: string; userEmail: string; }> { const userId = userInfo.sub || userInfo.preferred_username || 'unknown'; const userEmail = getUserEmail(userInfo); const sessionToken = await createSession(userId, userEmail); const sessionCookie = createSessionCookie(sessionToken); const clearStateCookie = 'auth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0'; return { sessionToken, sessionCookie, clearStateCookie, userId, userEmail }; } export async function withAuth(Astro: AstroGlobal, context: AuthContextType = 'general'): Promise { const authRequired = getAuthRequirement(context); console.log(`[DEBUG PAGE] Auth required for ${context}:`, authRequired); console.log('[DEBUG PAGE] Request URL:', Astro.url.toString()); if (!authRequired) { return { authenticated: true, session: null, userEmail: 'anon@anon.anon', userId: 'anonymous' }; } const sessionToken = getSessionFromRequest(Astro.request); console.log('[DEBUG PAGE] Session token found:', !!sessionToken); if (!sessionToken) { console.log('[DEBUG PAGE] No session token, redirecting to login'); const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(Astro.url.toString())}`; return new Response(null, { status: 302, headers: { 'Location': loginUrl } }); } const session = await verifySession(sessionToken); console.log('[DEBUG PAGE] Session verification result:', !!session); if (!session) { console.log('[DEBUG PAGE] Session verification failed, redirecting to login'); const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(Astro.url.toString())}`; return new Response(null, { status: 302, headers: { 'Location': loginUrl } }); } console.log(`[DEBUG PAGE] Page authentication successful for ${context}:`, session.userId); return { authenticated: true, session, userEmail: session.email, userId: session.userId }; } export async function withAPIAuth(request: Request, context: AuthContextType = 'general'): Promise<{ authenticated: boolean; userId: string; session?: SessionData; authRequired: boolean; }> { const authRequired = getAuthRequirement(context); if (!authRequired) { return { authenticated: true, userId: 'anonymous', authRequired: false }; } const sessionToken = getSessionFromRequest(request); console.log(`[DEBUG API] Session token found for ${context}:`, !!sessionToken); if (!sessionToken) { console.log(`[DEBUG API] No session token found for ${context}`); return { authenticated: false, userId: '', authRequired: true }; } const session = await verifySession(sessionToken); console.log(`[DEBUG API] Session verification result for ${context}:`, !!session); if (!session) { console.log(`[DEBUG API] Session verification failed for ${context}`); return { authenticated: false, userId: '', authRequired: true }; } console.log(`[DEBUG API] Authentication successful for ${context}:`, session.userId); return { authenticated: true, userId: session.userId, session, authRequired: true }; } export function getAuthRequirementForContext(context: AuthContextType): boolean { return getAuthRequirement(context); }