Bỏ qua nội dung

Đăng xuất (revoke refresh token)

Status
shipped
Priority
P1
Owner
team-identity
Platforms
fe · be · mobile
AC progress
7 / 7
Last reviewed
2026-05-22

Mục tiêu

Cho phép user đăng xuất khỏi phiên hiện tại bằng cách vô hiệu hoá refresh token đang dùng. Sau khi đăng xuất, refresh token không còn rotate được — user phải đăng nhập lại để có cặp token mới.

Phạm vi

Trong phạm vi (In scope):

  • Endpoint POST /auth/logout nhận refresh token và revoke nó
  • Idempotent: gọi nhiều lần cho cùng token không gây lỗi
  • Trả 204 No Content khi xử lý xong (kể cả token đã revoke trước đó)
  • Áp dụng cho mọi nguồn refresh token (đăng nhập phone+password hoặc Google OAuth)

Ngoài phạm vi (Out of scope):

  • Logout tất cả thiết bị (revoke tất cả refresh token của user) — cần endpoint riêng nếu cần
  • Vô hiệu access token đã phát hành (JWT stateless, không revoke được — chỉ chờ hết hạn 15 phút)
  • Xoá HttpOnly cookie ở browser (FE phải tự gọi clear cookie hoặc set cookie expired)
  • Lưu lịch sử logout cho audit

User Stories

  • user, tôi muốn bấm “Đăng xuất” để kết thúc phiên hiện tại an toàn, đảm bảo người khác dùng thiết bị này sau không vào được tài khoản tôi.
  • FE/Mobile dev, tôi muốn gọi logout nhiều lần (race condition, retry) không bị lỗi — đảm bảo cleanup luôn thành công.

Luồng chức năng

Vòng đời của một refresh token (state diagram):

stateDiagram-v2
    [*] --> ACTIVE: Đăng nhập / Đăng ký thành công

    ACTIVE --> ACTIVE: Refresh (rotate sang token mới)
    ACTIVE --> REVOKED: User logout
    ACTIVE --> EXPIRED: Quá 7 ngày không dùng

    REVOKED --> REVOKED: Logout lại (idempotent)
    EXPIRED --> [*]: Tự dọn

    REVOKED --> [*]: Tự dọn

Luồng request đăng xuất:

sequenceDiagram
    actor User
    participant App as FE/Mobile
    participant BE

    User->>App: Bấm "Đăng xuất"
    App->>BE: POST /auth/logout {refreshToken}
    BE->>BE: Tìm refresh token trong DB

    alt Token tồn tại và đang ACTIVE
        BE->>BE: Đánh dấu REVOKED
        BE-->>App: 204 No Content
    else Token đã REVOKED hoặc không tồn tại
        BE-->>App: 204 No Content (idempotent — không lỗi)
    end

    App->>App: Xoá local storage / cookie
    App->>User: Chuyển về màn hình đăng nhập

Acceptance Criteria

  • AC-1: Endpoint POST /auth/logout nhận body {refreshToken: string}.
  • AC-2: Refresh token trong body không được rỗng (validate @NotBlank) — rỗng → 400.
  • AC-3: Token hợp lệ và đang ACTIVE → đánh dấu REVOKED, trả 204 No Content.
  • AC-4: Token đã REVOKED trước đó (gọi logout lần 2+) → vẫn trả 204 No Content, KHÔNG lỗi (idempotent).
  • AC-5: Token không tồn tại trong DB (forged, expired đã xoá) → vẫn trả 204 No Content (không tiết lộ token nào có/không).
  • AC-6: Endpoint là public (không cần auth header) — chỉ cần biết refresh token là logout được.
  • AC-7: Logout chỉ ảnh hưởng refresh token cụ thể trong request — không revoke các refresh token khác của cùng user (multi-device support).

Quy tắc nghiệp vụ

  • Idempotent là bắt buộc: client có thể retry logout (vd lỗi mạng, FE call nhiều lần do race) — server không được phép trả lỗi cho lần 2+.
  • Access token (JWT) không bị invalidate qua endpoint này — nó stateless, chỉ chờ hết 15 phút. Trong thời gian đó nếu attacker có access token vẫn dùng được. Đây là trade-off thiết kế JWT-based.
  • Multi-device: user có thể có nhiều refresh token đang active (mỗi thiết bị 1). Logout chỉ vô hiệu token của thiết bị gửi request, các thiết bị khác vẫn đăng nhập.
  • Sau logout, để vào lại app user PHẢI đăng nhập (phone+password hoặc Google) — không có cơ chế “đăng nhập tự động” ở repo này.
  • Không tiết lộ thông tin về user qua endpoint logout (vd “token này thuộc user nào”) — chỉ 204 generic.

Dữ liệu & Trạng thái

Entity nghiệp vụ:

  • RefreshToken: token hash, userId, expiresAt, status (ACTIVE / REVOKED / EXPIRED), parentTokenId (cho rotation chain).

Trạng thái refresh token:

  • ACTIVE — đang dùng được, có thể rotate (xem feature liên quan refresh — chưa map).
  • REVOKED — user đã logout, không rotate được nữa.
  • EXPIRED — quá TTL (7 ngày), tự coi như invalid, dọn dần khỏi DB.

Phân loại response:

  • 204 No Content — luôn luôn (thành công hoặc idempotent no-op)
  • 400 — body sai format / refresh token rỗng

Câu hỏi mở

  • ❓ Có cần endpoint riêng “logout-all-devices” để revoke tất cả refresh token của user (vd khi nghi ngờ bị lộ tài khoản, đổi password)?
  • ❓ Có nên log audit event mỗi lần logout (analytics, security investigation)?
  • ❓ Sau logout, có cần TTL ngắn (vài giây) cho access token còn dùng để FE wrap-up state, hay accept rằng access vẫn còn 15 phút?
  • ❓ Nếu FE quên xoá HttpOnly cookie sau logout, behavior khi user F5 (cookie vẫn gửi)? Có nên BE clear cookie?

Liên quan

  • Phụ thuộc: Chưa có
  • Ảnh hưởng: Chưa có