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
80 lines
2.6 KiB
TypeScript
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');
|
|
}
|
|
}
|