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>
This commit is contained in:
2026-10-12 16:57:50 +02:00
co-authored by Claude Opus 5.5
parent 99357d59bd
commit 6d6b0ff121
15 changed files with 871 additions and 8 deletions
+35 -1
View File
@@ -1,5 +1,8 @@
import type { Metadata } from "next";
import { oeffentlicheAdresse } from "@/lib/adressen";
import { Karte } from "@/components/karte";
import { Status } from "@/components/status";
import { oidcEinstellungen } from "@/server/auth/oidc";
import { darf } from "@/server/auth/rechte";
import { seiteSchuetzen } from "@/server/auth/zugang";
import { benutzerListe } from "@/server/services/benutzer";
@@ -13,8 +16,10 @@ import { TokenVerwaltung } from "./token-verwaltung";
export const metadata: Metadata = { title: "Einstellungen" };
/** Einstellungen: eigene SSH-Schlüssel, CLI-Tokens, Passwort; für Admins die Benutzer. */
export default async function Einstellungen() {
export default async function Einstellungen({ searchParams }: { searchParams: Promise<{ sso?: string }> }) {
const benutzer = await seiteSchuetzen("eigeneZugaenge", "/einstellungen");
const sso = oidcEinstellungen();
const ssoFehler = (await searchParams).sso;
const admin = darf(benutzer.rolle, "benutzerVerwalten");
return (
<div className="flex flex-col gap-6">
@@ -30,6 +35,35 @@ export default async function Einstellungen() {
adresse={oeffentlicheAdresse("/", "http://localhost:3000").origin}
/>
{benutzer.passwortHash ? <PasswortAendern /> : null}
{sso ? (
<Karte titel="Anmeldung per SSO">
<div className="flex flex-col gap-3">
{ssoFehler === "schon_verknuepft" ? (
<Status art="fehler">Dieses SSO-Konto gehört schon zu einem anderen Benutzer.</Status>
) : null}
{benutzer.oidcSubjekt ? (
<Status art="erledigt">
Mit {sso.anzeigename} verknüpft – die Rolle kommt bei jeder SSO-Anmeldung aus den Gruppen.
</Status>
) : (
<>
<p className="text-muted text-sm">
Dein Konto ist noch nicht mit {sso.anzeigename} verknüpft. Danach kannst du dich auch dort
anmelden; die Rolle kommt dann aus den Gruppen beim Identity Provider.
</p>
<div>
<a
href="/api/v1/auth/oidc/start?verknuepfen=1"
className="border-linie bg-panel-hoch text-text hover:border-muted inline-flex h-11 items-center rounded-lg border px-4 text-sm font-semibold sm:h-10"
>
Mit {sso.anzeigename} verknüpfen
</a>
</div>
</>
)}
</div>
</Karte>
) : null}
{admin ? <BenutzerVerwaltung anfang={await benutzerListe()} ich={benutzer.benutzername} /> : null}
</div>
);