import { createHash, randomBytes } from "node:crypto"; import { cookies } from "next/headers"; import type { NextRequest } from "next/server"; import { isAuthEnabled } from "@/lib/auth/actor"; import { assertHostMatchesBaseUrl, discover, getOidcConfig, isOidcEnabled, OIDC_FLOW_COOKIE, OIDC_FLOW_MINUTES, redirectUri, } from "@/lib/auth/oidc"; import { errorResponse } from "@/lib/http"; import { QrRequestError } from "@/lib/qr/type-module"; export const dynamic = "force-dynamic"; function base64url(buffer: Buffer): string { return buffer.toString("base64url"); } /** * Schickt den Browser zum Anbieter. * * Mit PKCE (RFC 7636): Der Code allein nützt niemandem, weil beim Einlösen * zusätzlich das Geheimnis aus diesem Aufruf nötig ist. Das schützt auch dann, * wenn der Code unterwegs sichtbar wird — etwa in einem Proxy-Protokoll. */ export async function GET(request: NextRequest): Promise { try { if (!isAuthEnabled()) { throw new QrRequestError( "Die Anmeldung ist ausgeschaltet (AUTH_ENABLED=false).", 400, ); } if (!isOidcEnabled()) { throw new QrRequestError( "Die Anmeldung über einen externen Anbieter ist ausgeschaltet (OIDC_ENABLED=false).", 400, ); } // Vor allem anderen: Kommt der Browser über die Adresse, auf die // BASE_URL zeigt? Sonst geht der Vorgang auf dem Rückweg verloren. assertHostMatchesBaseUrl(request.headers); const config = getOidcConfig(); const meta = await discover(config.issuer); const state = base64url(randomBytes(32)); const nonce = base64url(randomBytes(32)); const codeVerifier = base64url(randomBytes(32)); const codeChallenge = base64url( createHash("sha256").update(codeVerifier).digest(), ); const store = await cookies(); store.set(OIDC_FLOW_COOKIE, JSON.stringify({ state, nonce, codeVerifier }), { httpOnly: true, // `lax` genügt: Der Anbieter schickt per GET zurück, dabei wird der // Cookie mitgeschickt. sameSite: "lax", path: "/", secure: (process.env.BASE_URL ?? "").startsWith("https://"), maxAge: OIDC_FLOW_MINUTES * 60, }); const ziel = new URL(meta.authorization_endpoint); ziel.searchParams.set("response_type", "code"); ziel.searchParams.set("client_id", config.clientId); ziel.searchParams.set("redirect_uri", redirectUri()); ziel.searchParams.set("scope", config.scopes); ziel.searchParams.set("state", state); ziel.searchParams.set("nonce", nonce); ziel.searchParams.set("code_challenge", codeChallenge); ziel.searchParams.set("code_challenge_method", "S256"); return Response.redirect(ziel.toString(), 302); } catch (error) { return errorResponse(error); } }