Kern der Benutzerverwaltung. Passwoerter: - scrypt aus der Node-Standardbibliothek (N=32768, r=8, p=1), speicherhart und ohne zusaetzliches natives Modul - Format scrypt$N$r$p$salt$hash, damit sich die Parameter spaeter anheben lassen - fakeVerifyDelay verbrennt bei unbekanntem Benutzernamen dieselbe Rechenzeit, sonst liesse sich an der Antwortzeit ablesen, welche Konten existieren Passkeys ueber SimpleWebAuthn 13: - Anmeldung ohne Benutzernamen ueber auffindbare Credentials - Challenges liegen in der Datenbank und werden beim Einloesen geloescht, koennen also nicht erneut eingespielt werden - rpID und Origin kommen aus APP_URL Sitzungen: - Zufallstoken im HttpOnly-Cookie, in der Datenbank nur dessen Hash - Abmelden und Sperren wirken sofort, weil bei jeder Anfrage nachgeschlagen wird - anders als bei einem selbsttragenden Token Middleware: CSRF-Pruefung ueber Sec-Fetch-Site bzw. Origin, Sicherheitskopfzeilen inklusive CSP mit Per-Request-Nonce und strict-dynamic, dazu eine grobe Zugangsschranke. Letztere ist ausdruecklich keine Sicherheitsgrenze - im Edge-Runtime gibt es keine Datenbank. Verbindlich entscheiden requireUserPage und requireUserApi, serverseitig, in jeder Seite und jedem Endpunkt. config.ts liest APP_NAME, APP_TAGLINE und APP_URL zur Laufzeit statt ueber NEXT_PUBLIC_-Variablen. Die waeren beim Build fest ins Bundle geschrieben; so laesst sich dasselbe Docker-Image ohne Neubau umbenennen und unter einer anderen Adresse betreiben. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
/**
|
||
* Laufzeitkonfiguration.
|
||
*
|
||
* Bewusst ohne NEXT_PUBLIC_-Variablen: die wuerden beim Build fest in das
|
||
* JavaScript-Bundle geschrieben. So laesst sich dasselbe Docker-Image ohne
|
||
* Neubau anders benennen und unter einer anderen URL betreiben. Werte, die
|
||
* der Browser braucht, reicht das Layout als Props weiter.
|
||
*/
|
||
|
||
function trimmed(value: string | undefined): string | null {
|
||
const result = value?.trim();
|
||
return result ? result : null;
|
||
}
|
||
|
||
/** Anzeigename der Instanz – Seitentitel, Kopfzeile, Passkey-Dialog. */
|
||
export const APP_NAME = trimmed(process.env.APP_NAME) ?? "Nebula";
|
||
|
||
/** Kleine Zeile unter dem Namen in der Kopfzeile. */
|
||
export const APP_TAGLINE = trimmed(process.env.APP_TAGLINE) ?? "Vibe Coding";
|
||
|
||
const RAW_APP_URL = trimmed(process.env.APP_URL) ?? "http://localhost:3000";
|
||
|
||
function parseAppUrl(raw: string): URL {
|
||
try {
|
||
const url = new URL(raw);
|
||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||
throw new Error(`APP_URL muss http oder https sein, ist aber "${url.protocol}"`);
|
||
}
|
||
return url;
|
||
} catch (error) {
|
||
throw new Error(
|
||
`APP_URL ist keine gueltige URL: "${raw}". Beispiel: https://nebula.example.com` +
|
||
` (${error instanceof Error ? error.message : String(error)})`,
|
||
);
|
||
}
|
||
}
|
||
|
||
const APP_URL_PARSED = parseAppUrl(RAW_APP_URL);
|
||
|
||
/** Vollstaendige Basis-URL ohne abschliessenden Slash. */
|
||
export const APP_URL = APP_URL_PARSED.origin;
|
||
|
||
/** Origin, den WebAuthn in der Assertion erwartet. */
|
||
export const WEBAUTHN_ORIGIN = APP_URL_PARSED.origin;
|
||
|
||
/**
|
||
* Relying-Party-ID fuer WebAuthn: der reine Hostname ohne Port.
|
||
* Passkeys sind an diesen Wert gebunden – aendert er sich, sind bestehende
|
||
* Passkeys nicht mehr verwendbar.
|
||
*/
|
||
export const WEBAUTHN_RP_ID = APP_URL_PARSED.hostname;
|
||
|
||
/**
|
||
* WebAuthn verlangt einen sicheren Kontext. Ausnahme: localhost gilt in
|
||
* Browsern als sicher, auch ohne TLS.
|
||
*/
|
||
export const WEBAUTHN_AVAILABLE =
|
||
APP_URL_PARSED.protocol === "https:" ||
|
||
APP_URL_PARSED.hostname === "localhost" ||
|
||
APP_URL_PARSED.hostname === "127.0.0.1";
|
||
|
||
/** Cookies nur ueber TLS ausliefern, wenn die App per https laeuft. */
|
||
export const COOKIES_SECURE = APP_URL_PARSED.protocol === "https:";
|
||
|
||
/** Lebensdauer einer Sitzung. */
|
||
export const SESSION_TTL_DAYS = Number(process.env.SESSION_TTL_DAYS ?? 30);
|
||
export const SESSION_TTL_MS = SESSION_TTL_DAYS * 24 * 60 * 60 * 1000;
|
||
|
||
export const SESSION_COOKIE = "nebula_session";
|
||
|
||
/** Wird beim Start einmal geloggt, damit Fehlkonfiguration frueh auffaellt. */
|
||
export function describeConfig(): string {
|
||
return [
|
||
`name=${APP_NAME}`,
|
||
`url=${APP_URL}`,
|
||
`rpId=${WEBAUTHN_RP_ID}`,
|
||
`passkeys=${WEBAUTHN_AVAILABLE ? "moeglich" : "deaktiviert (kein sicherer Kontext)"}`,
|
||
`secureCookies=${COOKIES_SECURE}`,
|
||
].join(" ");
|
||
}
|