- 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
23 lines
2.0 KiB
JSON
23 lines
2.0 KiB
JSON
{
|
|
"feature_id": "F-005",
|
|
"agent": "security",
|
|
"verdict": "APPROVED",
|
|
"summary": "El servidor es la única autoridad de identidad. Credenciales solo como argon2id PHC, sesiones opacas con hash persistido, cookies HttpOnly/Secure/SameSite, anti-enumeración con timing igualado y 401 genérico, brute force contenido con 429 + Retry-After. Sin hallazgos bloqueantes.",
|
|
"checks": {
|
|
"credential_storage": "PASS: argon2id m=19456KiB t=2 p=1 (OWASP 2024); integration test prueba que el plaintext no aparece en ningún valor de la fila",
|
|
"user_enumeration": "PASS: 401 genérico idéntico para email inexistente y password incorrecto (assert + curl live); dummy-hash verify memoizado iguala timing",
|
|
"sql_injection": "PASS: todas las queries parametrizadas; email validado por zod + citext UNIQUE en DB (race-safe)",
|
|
"session_token": "PASS: 512 bits crypto.randomBytes base64url; solo SHA-256 hex persiste (token_hash UNIQUE); fuga de DB no entrega sesiones utilizables",
|
|
"cookie_flags": "PASS: HttpOnly + Secure + SameSite=Lax + Path=/ + Max-Age=604800 verificados en vivo; COOKIE_SECURE default true",
|
|
"revocation": "PASS: logout revoca server-side (WHERE revoked_at IS NULL AND expires_at > now()); expiración en SQL, no en memoria",
|
|
"brute_force": "PASS: 10 fallos por email -> 429 + Retry-After: 900 (verificado en vivo); rate check corre ANTES del lookup en DB",
|
|
"input_abuse": "PASS: password max 128 chars limita costo argon2 por request; validación zod antes del dominio",
|
|
"secrets": "PASS: sin secretos hardcodeados; .env gitignored; errores de config nombran variables, nunca valores"
|
|
},
|
|
"residual_risks_accepted": [
|
|
"Sesiones concurrentes ilimitadas por usuario (sin cap). Aceptable en este slice.",
|
|
"Rate limiter in-memory: se pierde con restart y es por instancia. Documentado; interfaz lista para Redis.",
|
|
"Lockout temporal: un atacante puede bloquear un email 15 min (trade-off estándar); cooldown acotado lo contiene."
|
|
]
|
|
}
|