SPEC user-auth v3 · updated 2026-05-14 · 4 stories · 9 AS Active

user-auth

Email + password authentication with session cookies, password reset, and optional remember-me.

TL;DR

4 stories, 9 AS. Quyết định lớn: session cookie (không JWT), bcrypt cost 12, password tối thiểu 12 ký tự, rate limit 5 lần / 15 phút theo IP+email.

  • S-001 Login — happy path + sai password + rate-limited (3 AS, P0)
  • S-002 Logout — server-side session invalidation + cookie clear (2 AS, P0)
  • S-003 Password reset — email link, token 1h TTL (3 AS, P1)
  • S-004 Remember-me — 30d rolling cookie (1 AS, P2)

Overview

Hệ thống auth cho khu vực dashboard. Đăng nhập bằng email + password, session lưu server-side, cookie HTTP-only. Mục tiêu: replace middleware cũ (legacy token storage không meet compliance).

Data Model

Entities: User, Session, PasswordResetToken. Session liên kết 1-N với User, TTL 30 ngày, rolling khi có activity. PasswordResetToken single-use, TTL 1h, invalidate hết session khác khi reset thành công.

Stories

P0 S-001 Login với email + password 3 AS

User nhập email + password → nhận session cookie, redirect tới /dashboard. Sai password hiện lỗi generic. Quá 5 lần sai trong 15 phút → tạm khoá.

AS-001 Login thành công với credential đúng
Given
User jane@acme.com tồn tại, password đúng, không có session active
When
POST /api/login với {email, password}
Then
Response 200, set cookie sid (HTTP-only, Secure, SameSite=Lax), session row được tạo trong DB
Data: email=jane@acme.com, password=correct-horse-battery-staple
AS-002 Sai password trả lỗi generic, không leak user tồn tại
Given
User tồn tại nhưng password sai (hoặc user không tồn tại)
When
POST /api/login
Then
Response 401 với message "Invalid email or password". Không phân biệt 2 trường hợp ở message hay timing.
AS-003 Quá 5 lần sai → 429 trong 15 phút
Given
5 lần login fail liên tiếp cho cùng email từ cùng IP trong 15 phút
When
Lần thứ 6 POST /api/login
Then
Response 429 với header Retry-After: 900, dù password đúng cũng từ chối
Edge: đếm theo cặp (email, IP) — tránh attacker spam từ IP khác khoá user thật
P0 S-002 Logout 2 AS

User click logout → session xoá khỏi DB, cookie clear, redirect /login.

AS-004 Logout xoá session DB + clear cookie
Given
Session active, cookie sid hợp lệ
When
POST /api/logout
Then
Session row deleted, Set-Cookie: sid=; Max-Age=0, 302 → /login
AS-005 Logout khi không có session vẫn 200 (idempotent)
Given
Không có cookie sid hoặc session đã expire
When
POST /api/logout
Then
Response 200, không lỗi. Idempotent (gloss: gọi nhiều lần kết quả như gọi 1 lần).
P1 S-003 Password reset qua email 3 AS

User nhập email → nhận link reset (TTL 1h, single-use) → đặt password mới → invalidate tất cả session khác.

AS-006 Request reset link gửi email (luôn 200 dù email tồn tại hay không)
Given
Form reset password
When
POST /api/password-reset/request với {email}
Then
Response 200 với message generic "Check your email". Nếu email tồn tại → gửi link, không tồn tại → no-op (không leak).
AS-007 Token expired (>1h) trả lỗi
Given
Token tạo cách đây 65 phút
When
POST /api/password-reset/confirm với token + new password
Then
Response 410 "Token expired"
AS-008 Reset thành công invalidate tất cả session khác
Given
User có 3 session active (laptop, phone, tablet), token còn hạn
When
Reset password thành công
Then
3 session bị xoá, user phải login lại trên cả 3 thiết bị, token reset bị mark used.
P2 S-004 Remember-me cookie 1 AS

Checkbox "Remember me" trong form login → session TTL 30 ngày rolling thay vì 24h.

AS-009 Tick remember → cookie Max-Age 30 ngày
Given
Form login có checkbox "Remember me" được tick
When
Login thành công
Then
Cookie sidMax-Age=2592000 (30 ngày), session row có extended=true.

Constraints & Invariants

Password storage

Bcrypt cost 12 cho mọi password. Không dùng SHA*, không dùng PBKDF2. Migration từ legacy phải re-hash lúc login lần đầu.

Session cookie

Luôn HttpOnly, Secure, SameSite=Lax. Domain scope tới apex, không subdomain. Cookie name: sid.

Password policy

Tối thiểu 12 ký tự, kiểm tra zxcvbn score ≥ 3. Không bắt buộc ký tự đặc biệt (NIST 800-63B).

What Already Exists

Legacy auth middleware ở src/middleware/auth-legacy.ts đang dùng JWT lưu trong localStorage — đây là cái cần thay (compliance flagged). Bcrypt utility đã có ở src/lib/crypto/hash.ts, reuse. Rate limit helper src/lib/rate-limit.ts đã có cho API publicly-facing — reuse cho login.

Not in Scope

Change Log

3 entries expand để xem
DateChangeRef
2026-05-14Add S-004 Remember-me (P2)
2026-05-08S-003 priority P2 → P1 sau review complianceJIRA-1428
2026-04-30Initial creation

Snapshots

2 snapshots history
2026-05-08 snapshots/2026-05-08-JIRA-1428.md M3: priority change
2026-04-30 snapshots/2026-04-30.md M1: initial