Authentifizierung mit serverseitigen Sitzungen, CSRF und Rollen

Passwoerter mit Argon2id, im Cookie steht nur ein Token und in der Datenbank
dessen Hash. Das CSRF-Token wird per HMAC aus dem Sitzungstoken abgeleitet
statt separat gespeichert - ohne das httpOnly-Cookie ist es nicht herstellbar.

TOTP ist optional und wird erst nach einem bestaetigten Code scharf geschaltet,
damit sich niemand bei der Einrichtung aussperrt.
This commit is contained in:
2026-09-18 11:33:25 +02:00
parent a1b764155a
commit 3d2b87297e
7 changed files with 336 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
import { z } from 'zod';
const schema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.coerce.number().int().positive().default(3000),
HOST: z.string().default('0.0.0.0'),
DATABASE_URL: z.string().min(1, 'DATABASE_URL fehlt'),
SESSION_SECRET: z.string().min(32, 'SESSION_SECRET muss mindestens 32 Zeichen lang sein'),
// Lebensdauer einer Session in Stunden.
SESSION_TTL_HOURS: z.coerce.number().int().positive().default(12),
// Wird beim ersten Start angelegt, solange noch kein Account existiert.
BOOTSTRAP_ADMIN_USER: z.string().min(3).default('admin'),
BOOTSTRAP_ADMIN_PASSWORD: z.string().min(12).optional(),
// Hinter einem Reverse Proxy noetig, damit Client-IPs im Audit-Log stimmen.
TRUST_PROXY: z
.string()
.default('true')
.transform((v) => v === 'true' || v === '1'),
// Muss in Produktion true sein (Cookies nur ueber HTTPS).
COOKIE_SECURE: z
.string()
.optional()
.transform((v) => (v === undefined ? undefined : v === 'true' || v === '1')),
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
});
const parsed = schema.safeParse(process.env);
if (!parsed.success) {
const issues = parsed.error.issues.map((i) => ` - ${i.path.join('.')}: ${i.message}`).join('\n');
console.error(`Konfiguration unvollstaendig:\n${issues}`);
process.exit(1);
}
const raw = parsed.data;
export const env = {
...raw,
isProduction: raw.NODE_ENV === 'production',
// In Produktion ohne expliziten Wert: sichere Cookies erzwingen.
cookieSecure: raw.COOKIE_SECURE ?? raw.NODE_ENV === 'production',
};
export type Env = typeof env;