Files
0pt/app/api/auth/callback/route.ts
admin 35937933b3
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
masterplan-lock / check (push) Successful in 8s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / security (push) Successful in 23s
Security Scan (SIAX Cloud) / security (push) Successful in 23s
CI (SIAX Cloud) / integration (pull_request) Successful in 28s
CI (SIAX Cloud) / quality (pull_request) Successful in 46s
CI (SIAX Cloud) / quality (push) Successful in 54s
chore(auth): log the OIDC failure code (no tokens or claims)
2026-11-02 15:58:52 +01:00

80 lines
2.6 KiB
TypeScript

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');
}
}