feat(identity): F-005 register/login/logout with argon2 sessions and rate limiting

- Hexagonal identity module: domain ports, use cases, argon2id hasher, pg repos
- Migration 002_identity: identity_users + identity_sessions (token hash only)
- Opaque 512-bit session tokens; DB stores SHA-256 hash; 7-day TTL in SQL
- Cookie HttpOnly + Secure (COOKIE_SECURE, default true) + SameSite=Lax
- LoginRateLimiter: 10 failures -> 429 + Retry-After, 15-min cooldown
- Anti-enumeration: identical generic 401 + dummy-hash timing equalization
- buildApp gains optional pool/cookieSecure; foundation-only app preserved
- 47 unit + 14 integration tests; live smoke covers all acceptance criteria
This commit is contained in:
rikrdo
2026-08-14 22:58:32 +02:00
parent 4851692031
commit 75293f39bc
46 changed files with 1734 additions and 54 deletions

View File

@@ -16,6 +16,8 @@ export interface AppConfig {
logLevel: string;
databaseUrl: string;
redisUrl?: string;
/** Session cookie Secure flag. Default true; set COOKIE_SECURE=false for local http dev. */
cookieSecure: boolean;
/** Initial flag state parsed from FLAG_* vars. */
flags: Readonly<Record<string, boolean>>;
}
@@ -71,6 +73,15 @@ export function loadConfig(env: StringRecord): AppConfig {
const redisUrl = env.REDIS_URL && env.REDIS_URL !== '' ? env.REDIS_URL : undefined;
let cookieSecure = true;
const rawCookieSecure = env.COOKIE_SECURE;
if (rawCookieSecure !== undefined && rawCookieSecure !== '') {
const normalized = rawCookieSecure.trim().toLowerCase();
if (normalized === 'true') cookieSecure = true;
else if (normalized === 'false') cookieSecure = false;
else problems.push('COOKIE_SECURE must be "true" or "false"');
}
const flags: Record<string, boolean> = {};
for (const [key, raw] of Object.entries(env)) {
if (!key.startsWith('FLAG_') || raw === undefined) continue;
@@ -92,6 +103,7 @@ export function loadConfig(env: StringRecord): AppConfig {
logLevel,
databaseUrl: databaseUrl as string,
redisUrl,
cookieSecure,
flags,
};
}