- 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>
110 lines
3.5 KiB
TypeScript
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,
|
|
});
|
|
}
|