import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse, } from "@simplewebauthn/server"; import type { RegistrationResponseJSON, AuthenticationResponseJSON, AuthenticatorTransportFuture, } from "@simplewebauthn/types"; import { BRAND } from "@/lib/brand"; /** * SEC3-b: WebAuthn/Passkey-Basis. Kapselt die RP-/Origin-Konfiguration (aus AUTH_URL bzw. * WEBAUTHN_*-Overrides) und die vier @simplewebauthn/server-Zeremonien mit unserer * base64url-Kodierung. Höhere Schichten (Actions/Route) kümmern sich um Challenge-Store * (Cookie) und Persistenz. */ export const RP_NAME = BRAND.name; /** Cookie-Name der kurzlebigen Login-Challenge (pre-session Passkey-Login). */ export const LOGIN_CHALLENGE_COOKIE = "wa_login_challenge"; function authUrl(): URL { return new URL(process.env.WEBAUTHN_ORIGIN || process.env.AUTH_URL || "http://localhost:3000"); } /** Relying-Party-ID = registrierbare Domain (ohne Protokoll/Port). */ export function rpID(): string { return process.env.WEBAUTHN_RP_ID || authUrl().hostname; } /** Erwarteter Origin der Zeremonie (Protokoll + Host + Port). */ export function rpOrigin(): string { return authUrl().origin; } export function toB64Url(bytes: Uint8Array): string { return Buffer.from(bytes).toString("base64url"); } export function fromB64Url(s: string): Uint8Array { return new Uint8Array(Buffer.from(s, "base64url")); } type StoredCred = { credentialId: string; transports: string[] }; /** Registrierungs-Optionen (Challenge) für einen angemeldeten Nutzer erzeugen. */ export function buildRegistrationOptions(params: { userId: string; userName: string; userDisplayName: string; existing: StoredCred[]; }) { return generateRegistrationOptions({ rpName: RP_NAME, rpID: rpID(), userID: params.userId, userName: params.userName, userDisplayName: params.userDisplayName, attestationType: "none", excludeCredentials: params.existing.map((c) => ({ id: fromB64Url(c.credentialId), type: "public-key", transports: c.transports as AuthenticatorTransportFuture[], })), authenticatorSelection: { residentKey: "preferred", userVerification: "preferred" }, }); } export function verifyReg(response: RegistrationResponseJSON, expectedChallenge: string) { return verifyRegistrationResponse({ response, expectedChallenge, expectedOrigin: rpOrigin(), expectedRPID: rpID(), requireUserVerification: false, }); } /** Authentifizierungs-Optionen (Challenge) — allowCredentials optional (Discoverable/Resident). */ export function buildAuthenticationOptions(allow: StoredCred[]) { return generateAuthenticationOptions({ rpID: rpID(), allowCredentials: allow.map((c) => ({ id: fromB64Url(c.credentialId), type: "public-key", transports: c.transports as AuthenticatorTransportFuture[], })), userVerification: "preferred", }); } export function verifyAuth(params: { response: AuthenticationResponseJSON; expectedChallenge: string; credential: { credentialId: string; publicKey: string; counter: bigint }; }) { return verifyAuthenticationResponse({ response: params.response, expectedChallenge: params.expectedChallenge, expectedOrigin: rpOrigin(), expectedRPID: rpID(), authenticator: { credentialID: fromB64Url(params.credential.credentialId), credentialPublicKey: fromB64Url(params.credential.publicKey), counter: Number(params.credential.counter), }, requireUserVerification: false, }); }