Anmeldung ueber Authentik und andere OIDC-Anbieter
Anmeldefluss mit PKCE (S256), State und Nonce. Das ID-Token wird gegen den JWKS des Anbieters geprueft — Signatur, Aussteller, Empfaenger, Ablauf und Nonce; alles Noetige kommt aus dem Discovery-Dokument, damit die Anbindung nicht auf Authentik festgelegt ist. Wiedererkannt wird jemand an der Kennung des Anbieters (sub), nicht an der E-Mail-Adresse — Adressen aendern sich. Ein bestehendes Konto mit derselben Adresse wird verknuepft statt verdoppelt, aber nur bei bestaetigter Adresse: sonst koennte sich beim Anbieter jemand eine fremde Adresse eintragen und damit ein hiesiges Konto uebernehmen. Uebernommen wird bewusst nur, wer jemand ist — plus die Instanz-Administration, wenn OIDC_ADMIN_GROUP gesetzt ist. Gruppen und Rollen bleiben Sache dieser Anwendung: die Gruppen im Verzeichnis bedeuten etwas anderes als die hier. Konten aus dem Verzeichnis haben kein Passwort mehr (passwordHash ist jetzt optional); Konto-Bereich und Passwort-Anmeldung sagen das statt an einer leeren Pruefung zu scheitern. Anleitung mit allen Feldern in Authentik: docs/authentik.md
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
import { isAuthEnabled } from "@/lib/auth/actor";
|
||||
import {
|
||||
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(): Promise<Response> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user