import { NextRequest, NextResponse } from 'next/server'; import { signSession, SESSION_COOKIE } from '@/lib/auth'; import { OIDC_SESSION_TTL_SECONDS, OIDC_STATE_COOKIE, appBaseUrl, exchangeCode, readOidcConfig, verifyIdToken, } from '@/lib/oidc'; export const runtime = 'nodejs'; function safeNext(value: unknown): string { if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//')) { return '/portal/dokument'; } return value; } /** GET /api/auth/callback — finish OIDC, verify the id_token, set the session. */ export async function GET(request: NextRequest) { const config = readOidcConfig(); const fail = (reason: string) => NextResponse.redirect(`${appBaseUrl()}/portal/dokument?auth_error=${encodeURIComponent(reason)}`); if (!config) return fail('auth_not_configured'); const state = request.nextUrl.searchParams.get('state'); const code = request.nextUrl.searchParams.get('code'); const error = request.nextUrl.searchParams.get('error'); if (error) return fail('provider_error'); if (!state || !code) return fail('missing_code'); const raw = request.cookies.get(OIDC_STATE_COOKIE)?.value; if (!raw) return fail('missing_state'); let stored: { state?: unknown; nonce?: unknown; verifier?: unknown; next?: unknown; ts?: unknown }; try { stored = JSON.parse(raw); } catch { return fail('bad_state'); } if ( stored.state !== state || typeof stored.nonce !== 'string' || typeof stored.verifier !== 'string' || typeof stored.ts !== 'number' || Date.now() - stored.ts > 10 * 60 * 1000 ) { return fail('bad_state'); } try { const { idToken, accessToken } = await exchangeCode(config, code, stored.verifier); const verified = await verifyIdToken(config, idToken, stored.nonce, accessToken); const session = await signSession({ sub: verified.sub, tenantId: verified.tenantId, email: verified.email ?? '', exp: Date.now() + OIDC_SESSION_TTL_SECONDS * 1000, }); const response = NextResponse.redirect(`${appBaseUrl()}${safeNext(stored.next)}`); response.cookies.set(SESSION_COOKIE, session, { httpOnly: true, sameSite: 'lax', secure: appBaseUrl().startsWith('https://'), path: '/', maxAge: OIDC_SESSION_TTL_SECONDS, }); response.cookies.set(OIDC_STATE_COOKIE, '', { path: '/', maxAge: 0 }); return response; } catch (error: unknown) { // Codes only (e.g. token_exchange_failed), never tokens or claims. console.error( '[0pt-auth] oidc callback failed:', error instanceof Error ? error.message : 'unknown_error', ); return fail('verification_failed'); } }