import { getCookie, deleteCookie, setCookie, getRequestURL } from 'h3' import { getOidcConfig, constantTimeCompare, validateRedirectPath, exchangeCode, } from '../../../utils/oidc.js' import { getDb } from '../../../utils/db.js' import { getSessionMaxAgeDays } from '../../../utils/constants.js' const DEFAULT_ROLE = process.env.OIDC_DEFAULT_ROLE || 'member' function getIdentifier(claims) { return claims?.email ?? claims?.preferred_username ?? claims?.name ?? claims?.sub ?? 'oidc-user' } export default defineEventHandler(async (event) => { const query = getQuery(event) const code = query?.code const state = query?.state if (!code || !state) throw createError({ statusCode: 400, message: 'Invalid request' }) const cookieRaw = getCookie(event, 'oidc_state') if (!cookieRaw) throw createError({ statusCode: 400, message: 'Invalid request' }) let stored try { stored = JSON.parse(cookieRaw) } catch { throw createError({ statusCode: 400, message: 'Invalid request' }) } if (!stored?.state || !constantTimeCompare(state, stored.state)) { throw createError({ statusCode: 400, message: 'Invalid request' }) } const config = await getOidcConfig() if (!config) throw createError({ statusCode: 500, message: 'OIDC not configured' }) const currentUrl = getRequestURL(event) const checks = { expectedState: state, expectedNonce: stored.nonce, pkceCodeVerifier: stored.codeVerifier, } let tokens try { tokens = await exchangeCode(config, currentUrl, checks) } catch { deleteCookie(event, 'oidc_state', { path: '/' }) throw createError({ statusCode: 401, message: 'Authentication failed' }) } deleteCookie(event, 'oidc_state', { path: '/' }) const claims = tokens.claims?.() if (!claims?.sub) throw createError({ statusCode: 401, message: 'Authentication failed' }) const issuer = process.env.OIDC_ISSUER ?? '' const { get, run } = await getDb() let user = await get( 'SELECT id, identifier, role FROM users WHERE oidc_issuer = ? AND oidc_sub = ?', [issuer, claims.sub], ) if (!user) { const id = crypto.randomUUID() const now = new Date().toISOString() const identifier = getIdentifier(claims) await run( 'INSERT INTO users (id, identifier, password_hash, role, created_at, auth_provider, oidc_issuer, oidc_sub) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [id, identifier, null, DEFAULT_ROLE, now, 'oidc', issuer, claims.sub], ) user = await get('SELECT id, identifier, role FROM users WHERE id = ?', [id]) } // Invalidate all existing sessions for this user to prevent session fixation await run('DELETE FROM sessions WHERE user_id = ?', [user.id]) const sessionDays = getSessionMaxAgeDays() const sid = crypto.randomUUID() const now = new Date() const expires = new Date(now.getTime() + sessionDays * 24 * 60 * 60 * 1000) await run( 'INSERT INTO sessions (id, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)', [sid, user.id, now.toISOString(), expires.toISOString()], ) setCookie(event, 'session_id', sid, { httpOnly: true, sameSite: 'strict', path: '/', maxAge: sessionDays * 24 * 60 * 60, secure: process.env.NODE_ENV === 'production', }) const redirectParam = query?.redirect const path = validateRedirectPath(redirectParam) return sendRedirect(event, path.startsWith('http') ? path : new URL(path, getRequestURL(event).origin).href, 302) })