Files
craftvia/src/server/webauthn.ts
T
msolarczekandClaude Opus 5 9ec7fa5356 Fundament: Branding Craftvia
- Farb-Tokens laut Brandbook §11.4 (Lotsenblau, Signalorange, Graphit, Hafengrau,
  Stahlgrau, Zink, Funktionsfarben) in globals.css + src/lib/brand.ts; helles Theme
- Inline-SVG-Logo src/components/brand/craftvia-logo.tsx (horizontal/signet,
  color/mono/reversed, App-Icon-Kachel, optionale Tagline); Certvia-/GEFIM-Logos entfernt
- Favicon/PWA-Icons aus dem Signet erzeugt (scripts/generate-brand-icons.ts),
  site.webmanifest (Craftvia, theme_color #082E5B)
- Inter selbst gehostet (next/font/local, lokale OFL-Datei), Open Sans/Poppins entfernt
- Metadaten, WebAuthn-RP-Name, Mail-/Dokument-CD auf Craftvia umgestellt
- docs/craftvia/BRANDING.md ersetzt docs/BRANDING-CERTVIA.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:39:52 +02:00

110 lines
3.5 KiB
TypeScript

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,
});
}