Files
proxdeploy/src/app/api/v1/auth/oidc/rueckruf/route.ts
T
florian.wildeandClaude Opus 5.5 6d6b0ff121 feat(auth): Anmeldung per OIDC mit Rollen aus Gruppen
Authorization Code mit PKCE, State und Nonce über openid-client 6.8.8; die Rolle kommt bei jeder Anmeldung aus dem Gruppen-Claim. Lokale Konten werden nie über den Namen übernommen, verknüpfen geht nur angemeldet. Getestet gegen einen Mock-IdP inklusive gefälschtem State, falscher Nonce und fremder Audience.

Closes #016

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:57:50 +02:00

66 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextResponse } from "next/server";
import { aufruferAdresse, istHttps, oeffentlicheAdresse } from "@/lib/adressen";
import { logger } from "@/lib/logger";
import { SITZUNG_COOKIE, SITZUNG_LAUFZEIT_STUNDEN, sitzungErzeugen } from "@/lib/sitzung";
import {
OIDC_COOKIE,
oidcAbschliessen,
oidcEinstellungen,
OidcFehler,
RUECKRUF_PFAD,
} from "@/server/auth/oidc";
function cookieLesen(kopf: string | null, name: string): string | undefined {
for (const teil of (kopf ?? "").split(";")) {
const [schluessel, ...wert] = teil.trim().split("=");
if (schluessel === name) return decodeURIComponent(wert.join("="));
}
return undefined;
}
/**
* Öffentlich: Rückkehr vom Identity Provider. Erfolg → Sitzungscookie und Weiterleitung zum Ziel;
* Fehler → Anmeldeseite mit Code (`?sso=keine_gruppe` …), nie eine Fehlerseite mit Interna.
*/
export async function GET(anfrage: Request): Promise<Response> {
const herkunft = new URL(anfrage.url).origin;
const e = oidcEinstellungen();
const zurAnmeldung = (code: string) =>
NextResponse.redirect(oeffentlicheAdresse(`/anmelden?sso=${code}`, herkunft), 302);
if (!e) return zurAnmeldung("aus");
// Die öffentliche Adresse: genau die, die als redirect_uri beim IdP registriert ist.
const aktuell = oeffentlicheAdresse(RUECKRUF_PFAD + new URL(anfrage.url).search, herkunft);
let antwort: NextResponse;
try {
const { benutzer, ziel } = await oidcAbschliessen(
e,
aktuell,
cookieLesen(anfrage.headers.get("cookie"), OIDC_COOKIE),
aufruferAdresse(anfrage.headers),
);
antwort = NextResponse.redirect(oeffentlicheAdresse(ziel, herkunft), 302);
antwort.cookies.set(
SITZUNG_COOKIE,
await sitzungErzeugen({ benutzerId: benutzer.id, herkunft: "oidc" }),
{
httpOnly: true,
sameSite: "lax",
secure: istHttps(anfrage.headers, anfrage.url),
path: "/",
maxAge: SITZUNG_LAUFZEIT_STUNDEN * 3600,
},
);
} catch (fehler) {
if (!(fehler instanceof OidcFehler)) logger.error({ err: fehler }, "OIDC-Rückruf fehlgeschlagen");
const code = fehler instanceof OidcFehler ? fehler.code : "idp";
// Beim Verknüpfen ist man schon angemeldet – die Meldung gehört dann in die Einstellungen.
antwort =
code === "schon_verknuepft"
? NextResponse.redirect(oeffentlicheAdresse(`/einstellungen?sso=${code}`, herkunft), 302)
: zurAnmeldung(code);
}
antwort.cookies.set(OIDC_COOKIE, "", { path: "/api/v1/auth/oidc", maxAge: 0 });
return antwort;
}