feat(auth): Anmeldung mit Passwort und Passkey
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1e9f7bd45a
commit
37573962a8
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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(" ");
|
||||
}
|
||||
Reference in New Issue
Block a user