- 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
30 lines
2.1 KiB
JSON
30 lines
2.1 KiB
JSON
{
|
||
"feature_id": "F-005",
|
||
"agent": "qa",
|
||
"verdict": "APPROVED",
|
||
"summary": "Los 6 criterios de aceptación pasan con evidencia doble: suite automatizada (47 unit + 14 integration) y smoke live contra PostgreSQL real. Sin regresiones en F-001..F-004.",
|
||
"acceptance_criteria": {
|
||
"AC1_login_200_with_cookie": "PASS - integration 'login with valid credentials -> 200 + secure cookie' + curl live: HTTP 200 + Set-Cookie mdv_session=...; Max-Age=604800; Path=/; HttpOnly; Secure; SameSite=Lax",
|
||
"AC2_wrong_password_401_no_enumeration": "PASS - integration compara bodies .error idénticos para wrong-password vs unknown-email; curl live confirma 401 INVALID_CREDENTIALS genérico en ambos",
|
||
"AC3_argon2_never_plaintext": "PASS - integration: fila DB password_hash arranca con $argon2id$ y el plaintext no aparece en ningún valor serializado de la fila",
|
||
"AC4_rate_limit_429": "PASS - integration: 10 fallos -> 429 TOO_MANY_ATTEMPTS + Retry-After > 0; live: 401×10 y luego 429 con retry-after: 900",
|
||
"AC5_cookie_flags": "PASS - integration valida HttpOnly + SameSite=Lax + Secure + Max-Age sobre el header Set-Cookie; confirmado en vivo",
|
||
"AC6_verify_green": "PASS - ./scripts/verify.sh OK tras el cierre de build+gates"
|
||
},
|
||
"regression": {
|
||
"health_endpoint": "PASS (suite)",
|
||
"error_envelope_v2_404": "PASS (suite) + foundation-only app sin pool mantiene /auth 404 (integration)",
|
||
"config_fail_fast": "PASS (suite: DATABASE_URL missing, COOKIE_SECURE inválido reporta variable)",
|
||
"feature_flags": "PASS (suite: unknown OFF, case-insensitive, runtime flip)",
|
||
"migrations_lifecycle": "PASS (integration: fresh up full schema, second up no-op, full down limpio)"
|
||
},
|
||
"evidence": [
|
||
"npm run lint / lint:boundaries (39 files) / typecheck / build: clean",
|
||
"npm test: 47 passed | 14 skipped",
|
||
"npm run test:integration: 14/14 passed",
|
||
"curl live: register 201, login 200+cookie, wrong/unknown 401 idénticos, logout 204+clear, 429+Retry-After",
|
||
"./scripts/verify.sh: OK"
|
||
],
|
||
"notes": "Logout smoke verificado: cookie borrada (Max-Age=0 + Expires 1970) y fila de sesión con revoked_at IS NOT NULL en DB."
|
||
}
|