# Passkeys Implementation Checklist

> **Version:** 1.0 | **Last Updated:** 2024 | **Standard:** WebAuthn Level 2 / FIDO2

---

## Tổng quan tiến độ

| Phần | Mục | Hoàn thành |
|------|-----|------------|
| 1. Chuẩn bị hạ tầng | 12 | ☐ |
| 2. Phía Backend | 18 | ☐ |
| 3. Phía Frontend | 16 | ☐ |
| 4. Kiểm thử trước Production | 14 | ☐ |
| **Tổng cộng** | **60** | **☐** |

---

## Phần 1 — Chuẩn bị hạ tầng

### 1.1 HTTPS & TLS

- [ ] **1.1.1** Toàn bộ domain sử dụng HTTPS (WebAuthn bắt buộc secure context)
- [ ] **1.1.2** TLS certificate hợp lệ, chưa hết hạn, được ký bởi CA tin cậy
- [ ] **1.1.3** Redirect toàn bộ HTTP → HTTPS (301 Permanent)
- [ ] **1.1.4** Cấu hình HSTS header: `Strict-Transport-Security: max-age=31536000; includeSubDomains`
- [ ] **1.1.5** Kiểm tra SSL Labs score ≥ A (https://www.ssllabs.com/ssltest)

### 1.2 Domain & RP (Relying Party) Configuration

- [ ] **1.2.1** Xác định `rpId` — phải là domain hoặc suffix hợp lệ của origin
  ```
  Ví dụ: origin = https://app.example.com → rpId = "app.example.com" hoặc "example.com"
  ```
- [ ] **1.2.2** Xác định `rpName` — tên hiển thị cho người dùng trên authenticator dialog
- [ ] **1.2.3** Nếu dùng subdomain strategy, kiểm tra `rpId` cover đúng các subdomain cần thiết
- [ ] **1.2.4** Cấu hình `/.well-known/webauthn` nếu cần cross-origin credential (optional nhưng recommended)
- [ ] **1.2.5** Đảm bảo `rpId` nhất quán giữa registration và authentication flow

### 1.3 Database Schema

- [ ] **1.3.1** Tạo bảng `passkey_credentials` với các cột tối thiểu:

  ```sql
  CREATE TABLE passkey_credentials (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id             UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    credential_id       BYTEA UNIQUE NOT NULL,      -- raw credential ID từ authenticator
    public_key          BYTEA NOT NULL,              -- COSE-encoded public key
    sign_count          BIGINT NOT NULL DEFAULT 0,  -- counter chống replay attack
    aaguid              UUID,                        -- authenticator model identifier
    transports          TEXT[],                      -- ['internal','hybrid','usb',...]
    device_name         VARCHAR(255),                -- tên thiết bị do user đặt
    backed_up           BOOLEAN DEFAULT FALSE,       -- passkey có được sync lên cloud không
    backup_eligible     BOOLEAN DEFAULT FALSE,
    created_at          TIMESTAMPTZ DEFAULT NOW(),
    last_used_at        TIMESTAMPTZ,
    revoked_at          TIMESTAMPTZ                  -- NULL = đang active
  );
  ```

- [ ] **1.3.2** Tạo index trên `credential_id` và `user_id`
  ```sql
  CREATE INDEX idx_passkey_credential_id ON passkey_credentials(credential_id);
  CREATE INDEX idx_passkey_user_id ON passkey_credentials(user_id);
  ```
- [ ] **1.3.3** Tạo bảng hoặc column lưu challenge tạm thời (TTL-based):
  ```sql
  CREATE TABLE webauthn_challenges (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    challenge   BYTEA UNIQUE NOT NULL,
    user_id     UUID REFERENCES users(id),   -- NULL cho authentication (trước khi biết user)
    type        VARCHAR(20) NOT NULL,         -- 'registration' | 'authentication'
    expires_at  TIMESTAMPTZ NOT NULL,         -- NOW() + INTERVAL '5 minutes'
    used        BOOLEAN DEFAULT FALSE
  );
  ```
- [ ] **1.3.4** Thêm cột `passkey_enabled` (boolean) vào bảng `users` nếu cần flag nhanh
- [ ] **1.3.5** Đảm bảo migration script có rollback plan

---

## Phần 2 — Phía Backend

### 2.1 Thư viện & Dependencies

- [ ] **2.1.1** Chọn và tích hợp thư viện WebAuthn phù hợp với stack:
  | Stack | Thư viện đề xuất |
  |-------|-----------------|
  | Node.js | `@simplewebauthn/server` |
  | Python | `py_webauthn` |
  | Go | `github.com/go-webauthn/webauthn` |
  | Java | `webauthn4j` |
  | Ruby | `webauthn-ruby` |
  | PHP | `web-auth/webauthn-lib` |

- [ ] **2.1.2** Verify thư viện đang dùng hỗ trợ WebAuthn Level 2
- [ ] **2.1.3** Pin version thư viện trong `package.json` / `requirements.txt` / `go.mod`

### 2.2 Challenge Generation

- [ ] **2.2.1** Challenge được tạo bằng **cryptographically secure random** (không dùng `Math.random()`)
  ```javascript
  // Node.js example
  import { generateChallenge } from '@simplewebauthn/server';
  const challenge = generateChallenge(); // 32 bytes, crypto-safe
  ```
- [ ] **2.2.2** Challenge có độ dài tối thiểu **16 bytes** (khuyến nghị 32 bytes)
- [ ] **2.2.3** Challenge được lưu server-side với TTL **≤ 5 phút**
- [ ] **2.2.4** Challenge là **one-time use** — đánh dấu `used = TRUE` ngay sau khi verify thành công
- [ ] **2.2.5** Challenge gửi về client dưới dạng Base64URL encoded
- [ ] **2.2.6** Implement cron job / cleanup để xóa expired challenges định kỳ

### 2.3 Registration Endpoint

- [ ] **2.3.1** `POST /auth/passkey/register/options` — trả về registration options:
  ```javascript
  // Các field bắt buộc kiểm tra
  {
    rp: { id: RP_ID, name: RP_NAME },
    user: { id: userIdBuffer, name: userEmail, displayName: userName },
    challenge: challengeBuffer,
    pubKeyCredParams: [
      { type: 'public-key', alg: -7 },   // ES256 (ECDSA P-256)
      { type: 'public-key', alg: -257 }, // RS256 (RSA)
    ],
    timeout: 60000,
    attestation: 'none', // 'none' | 'indirect' | 'direct'
    authenticatorSelection: {
      residentKey: 'preferred',       // 'required' | 'preferred' | 'discouraged'
      userVerification: 'preferred',  // 'required' | 'preferred' | 'discouraged'
    },
    excludeCredentials: [/* existing credentials của user để tránh đăng ký lặp */]
  }
  ```
- [ ] **2.3.2** `POST /auth/passkey/register/verify` — xử lý response từ authenticator
- [ ] **2.3.3** Verify `origin` khớp với expected origin (chống phishing)
- [ ] **2.3.4** Verify `rpIdHash` khớp với SHA-256 của `rpId`
- [ ] **2.3.5** Verify `challenge` trong clientDataJSON khớp với challenge đã lưu
- [ ] **2.3.6** Verify `UP` (User Present) flag = `1` trong authenticatorData
- [ ] **2.3.7** Verify `UV` (User Verified) flag nếu `userVerification: 'required'`
- [ ] **2.3.8** Parse và lưu `credentialId`, `publicKey`, `signCount`, `aaguid` vào DB
- [ ] **2.3.9** Lưu `backupEligible` và `backupState` flags
- [ ] **2.3.10** Giới hạn số lượng passkey tối đa mỗi user (khuyến nghị: 5-10)

### 2.4 Authentication Endpoint

- [ ] **2.4.1** `POST /auth/passkey/login/options` — trả về authentication options:
  ```javascript
  {
    challenge: challengeBuffer,
    timeout: 60000,
    rpId: RP_ID,
    allowCredentials: [], // [] = discoverable credential (passkey mode)
    userVerification: 'preferred',
  }
  ```
- [ ] **2.4.2** `POST /auth/passkey/login/verify` — xác thực assertion từ client
- [ ] **2.4.3** Lookup `credentialId` trong DB để tìm đúng credential
- [ ] **2.4.4** Verify signature bằng stored public key
- [ ] **2.4.5** Verify `challenge` khớp với challenge đã lưu server-side
- [ ] **2.4.6** Verify `origin` và `rpIdHash`

### 2.5 Counter Check (Chống Replay Attack)

- [ ] **2.5.1** Sau khi verify thành công, so sánh `newSignCount > storedSignCount`
- [ ] **2.5.2** Nếu `newSignCount == 0` → authenticator không support counter (chấp nhận nhưng log lại)
- [ ] **2.5.3** Nếu `newSignCount <= storedSignCount` và `newSignCount != 0` → **flag as anomaly**:
  - [ ] Log sự kiện bảo mật với đầy đủ context
  - [ ] Cân nhắc: block login, notify user, hoặc revoke credential tùy policy
- [ ] **2.5.4** Update `sign_count` trong DB với giá trị mới sau mỗi authentication thành công
- [ ] **2.5.5** Update `last_used_at` timestamp

### 2.6 Error Handling & Security

- [ ] **2.6.1** Tất cả WebAuthn errors trả về **generic message** phía client (không leak internal details)
  ```javascript
  // ✅ ĐÚNG
  { error: 'Authentication failed. Please try again.' }

  // ❌ SAI — leak thông tin
  { error: 'Invalid signature for credentialId: abc123, expected counter: 42' }
  ```
- [ ] **2.6.2** Rate limiting trên tất cả passkey endpoints (ví dụ: 10 requests/phút/IP)
- [ ] **2.6.3** Log đầy đủ cho security audit: `user_id`, `credential_id`, `ip`, `user_agent`, `timestamp`, `result`
- [ ] **2.6.4** API endpoints yêu cầu authentication session hợp lệ cho `register/options` (user đã đăng nhập)
- [ ] **2.6.5** Implement CSRF protection trên các mutation endpoints
- [ ] **2.6.6** Validate tất cả input trước khi xử lý (credentialId format, response size limits)
- [ ] **2.6.7** Timeout handling — trả về lỗi rõ ràng nếu challenge đã expired

---

## Phần 3 — Phía Frontend

### 3.1 Feature Detection

- [ ] **3.1.1** Kiểm tra `window.PublicKeyCredential` trước khi hiển thị passkey UI
  ```javascript
  const isWebAuthnSupported = () =>
    window.PublicKeyCredential !== undefined &&
    typeof window.PublicKeyCredential === 'function';
  ```
- [ ] **3.1.2** Kiểm tra platform authenticator (Face ID / Fingerprint) với async check:
  ```javascript
  const isPlatformAuthAvailable = async () => {
    if (!isWebAuthnSupported()) return false;
    return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
  };
  ```
- [ ] **3.1.3** Kiểm tra Conditional UI (autofill passkey) nếu muốn dùng tính năng này:
  ```javascript
  const isConditionalMediationAvailable = async () =>
    PublicKeyCredential.isConditionalMediationAvailable?.() ?? false;
  ```
- [ ] **3.1.4** Kết quả feature detection được cache trong session (tránh gọi lại nhiều lần)
- [ ] **3.1.5** Thực hiện feature detection **trước khi render UI** để tránh layout shift

### 3.2 UI — Nút & Flow Đăng ký Passkey

- [ ] **3.2.1** Nút "Tạo Passkey" chỉ hiển thị khi `isWebAuthnSupported() === true`
- [ ] **3.2.2** Copy rõ ràng giải thích passkey là gì trước khi user nhấn:
  ```
  Ví dụ: "Đăng nhập bằng vân tay, Face ID hoặc mã PIN thiết bị — không cần nhớ mật khẩu"
  ```
- [ ] **3.2.3** Loading state trong khi gọi `navigator.credentials.create()` (spinner / disabled button)
- [ ] **3.2.4** Xử lý và hiển thị lỗi user-friendly cho từng error code:
  | Error | Message hiển thị |
  |-------|-----------------|
  | `NotAllowedError` | "Yêu cầu bị huỷ hoặc hết thời gian. Vui lòng thử lại." |
  | `InvalidStateError` | "Thiết bị này đã được đăng ký." |
  | `NotSupportedError` | "Thiết bị không hỗ trợ tính năng này." |
  | `SecurityError` | "Lỗi bảo mật. Vui lòng kiểm tra kết nối HTTPS." |
  | Network error | "Không thể kết nối máy chủ. Kiểm tra mạng và thử lại." |

- [ ] **3.2.5** Sau khi đăng ký thành công: hiển thị confirmation với tên thiết bị / ngày tạo
- [ ] **3.2.6** Cho phép user đặt tên cho passkey vừa tạo (custom device name)

### 3.3 UI — Nút & Flow Đăng nhập Passkey

- [ ] **3.3.1** Nút "Đăng nhập bằng Passkey" prominently hiển thị trên login page
- [ ] **3.3.2** Implement Conditional UI (autofill) nếu browser hỗ trợ:
  ```javascript
  // Thêm autocomplete="webauthn" vào input username/email
  // <input type="email" autocomplete="username webauthn" />

  // Khởi tạo conditional flow
  if (await isConditionalMediationAvailable()) {
    navigator.credentials.get({
      mediation: 'conditional',
      publicKey: { challenge, rpId, userVerification: 'preferred' }
    });
  }
  ```
- [ ] **3.3.3** Loading state + progress indicator khi xác thực đang diễn ra
- [ ] **3.3.4** Abort controller để cancel conditional flow khi user chuyển sang password login:
  ```javascript
  const abortController = new AbortController();
  navigator.credentials.get({ signal: abortController.signal, ... });
  // Khi user click "Dùng mật khẩu": abortController.abort();
  ```

### 3.4 Fallback Flow

- [ ] **3.4.1** Nếu `isWebAuthnSupported() === false` → ẩn hoàn toàn passkey UI, hiển thị password form
- [ ] **3.4.2** Nút "Dùng mật khẩu thay thế" luôn accessible ngay cạnh passkey option
- [ ] **3.4.3** Nếu passkey authentication thất bại → auto-suggest fallback với message rõ ràng:
  ```
  "Không thể dùng passkey. Bạn có thể đăng nhập bằng mật khẩu hoặc email OTP."
  ```
- [ ] **3.4.4** Magic link / OTP email fallback có sẵn như lớp cuối cùng
- [ ] **3.4.5** Recovery flow cho trường hợp mất tất cả passkeys (account recovery page)
- [ ] **3.4.6** Không bao giờ force user dùng passkey nếu họ chưa muốn — passkey là thêm vào, không thay thế bắt buộc (trừ khi đó là product decision có chủ ý)

### 3.5 Quản lý Credential (Security Settings Page)

- [ ] **3.5.1** Trang `/settings/security/passkeys` liệt kê tất cả passkeys của user:
  - Tên thiết bị (editable)
  - Ngày tạo (`created_at`)
  - Lần dùng cuối (`last_used_at`)
  - Icon platform (iOS, Android, Windows, Security Key)
  - Badge "Backed up" nếu `backed_up = true`
- [ ] **3.5.2** Nút "Đổi tên" cho từng passkey (PATCH request cập nhật `device_name`)
- [ ] **3.5.3** Nút "Xoá" với confirmation dialog:
  ```
  "Xoá passkey này sẽ ngăn bạn đăng nhập bằng thiết bị [Tên thiết bị].
   Bạn vẫn có thể dùng mật khẩu. Xác nhận xoá?"
  ```
- [ ] **3.5.4** Warning nếu user sắp xoá passkey cuối cùng và chưa có phương thức khác
- [ ] **3.5.5** "Thêm Passkey" button trên trang này để đăng ký thêm thiết bị
- [ ] **3.5.6** Hiển thị trạng thái real-time (skeleton loading, empty state khi chưa có passkey)

---

## Phần 4 — Kiểm thử trước Production

### 4.1 Test trên nhiều thiết bị & Platform

- [ ] **4.1.1** **iOS Safari** (iPhone) — Face ID / Touch ID
  - [ ] Registration flow
  - [ ] Authentication flow
  - [ ] Conditional UI (autofill)
- [ ] **4.1.2** **Android Chrome** — Fingerprint / Face unlock
  - [ ] Registration flow
  - [ ] Authentication flow
  - [ ] Google Password Manager passkey sync
- [ ] **4.1.3** **macOS Safari** — Touch ID
- [ ] **4.1.4** **macOS Chrome / Edge** — Touch ID
- [ ] **4.1.5** **Windows 11 Chrome / Edge** — Windows Hello (PIN, Face, Fingerprint)
- [ ] **4.1.6** **Hardware Security Key** (YubiKey hoặc tương đương) — USB & NFC
- [ ] **4.1.7** **Cross-device flow** — Đăng nhập trên PC bằng QR code quét từ điện thoại (hybrid transport)
- [ ] **4.1.8** Test passkey sync: tạo passkey trên iOS → đăng nhập trên iPad khác cùng Apple ID

### 4.2 Test Các Trường hợp Biên (Edge Cases)

- [ ] **4.2.1** User huỷ dialog xác thực giữa chừng (`NotAllowedError`) → UI recover đúng
- [ ] **4.2.2** Challenge timeout (để > 5 phút rồi submit) → server reject, client hiển thị lỗi đúng
- [ ] **4.2.3** Đăng ký credential đã tồn tại (`InvalidStateError` / `excludeCredentials` hoạt động)
- [ ] **4.2.4** Network drop giữa request options và submit response → error handling graceful
- [ ] **4.2.5** Submit response của registration vào endpoint authentication và ngược lại → server reject
- [ ] **4.2.6** Replay attack simulation: submit cùng authentication response 2 lần → thứ 2 bị reject
- [ ] **4.2.7** Tamper `clientDataJSON` trước khi submit → server detect và reject

### 4.3 Test Fallback Flow

- [ ] **4.3.1** Disable WebAuthn trong browser (hoặc dùng browser cũ không hỗ trợ) → password form hiển thị, không có JS error
- [ ] **4.3.2** User có passkey nhưng chọn "Dùng mật khẩu" → flow hoạt động bình thường
- [ ] **4.3.3** Passkey authentication thất bại → fallback prompt xuất hiện đúng, không mất data
- [ ] **4.3.4** User không có passkey nào → login page không hiển thị passkey button
- [ ] **4.3.5** Magic link / OTP email fallback hoạt động end-to-end
- [ ] **4.3.6** Account recovery page hoạt động (email verification → reset passkeys)

### 4.4 Test Revoke & Security

- [ ] **4.4.1** Xoá một passkey qua Settings → credential không còn dùng được để login
- [ ] **4.4.2** Xoá tất cả passkeys → user vẫn login được bằng password
- [ ] **4.4.3** Admin revoke passkey của user (trong admin panel nếu có) → có hiệu lực ngay
- [ ] **4.4.4** Sau khi đổi mật khẩu → passkeys vẫn hoạt động (passkeys độc lập với password)
- [ ] **4.4.5** Session logout → passkey credentials không bị xoá (chỉ session token bị invalidate)
- [ ] **4.4.6** Kiểm tra audit log ghi nhận đúng các events: `passkey.created`, `passkey.used`, `passkey.revoked`

### 4.5 Performance & Monitoring

- [ ] **4.5.1** Đo thời gian response của các passkey endpoints (target: < 300ms không tính authenticator)
- [ ] **4.5.2** Thiết lập alert cho error rate cao trên passkey endpoints (ngưỡng: > 5%)
- [ ] **4.5.3** Dashboard metric: `passkey_registrations`, `passkey_authentications_success`, `passkey_authentications_failed`
- [ ] **4.5.4** Test expired challenge cleanup job hoạt động đúng
- [ ] **4.5.5** Load test endpoints với 100 concurrent requests — verify rate limiting hoạt động

---

## Pre-launch Final Checklist

> Tick toàn bộ trước khi merge vào `main` / deploy production

- [ ] Tất cả 60 mục trên đã được review và tick
- [ ] Security review bởi ít nhất 1 người khác (4-eyes principle)
- [ ] Penetration test hoặc WebAuthn-specific security audit đã thực hiện
- [ ] Documentation nội bộ về passkey flow đã được viết
- [ ] Runbook cho incident response (ví dụ: toàn bộ user bị lock out) đã sẵn sàng
- [ ] Feature flag / kill switch có thể tắt passkey feature mà không cần redeploy
- [ ] Rollback plan cho database migration đã test thực tế

---

## Tài liệu tham khảo

| Tài liệu | Link |
|----------|------|
| WebAuthn W3C Spec (Level 2) | https://www.w3.org/TR/webauthn-2/ |
| FIDO Alliance Passkey Overview | https://fidoalliance.org/passkeys/ |
| SimpleWebAuthn (Node.js) | https://simplewebauthn.dev |
| Google Passkey Codelab | https://developers.google.com/codelabs/passkey-form-autofill |
| Apple Passkey Documentation | https://developer.apple.com/passkeys/ |
| WebAuthn.me (Testing Tool) | https://webauthn.me |
| Passkeys.dev (Cross-platform guide) | https://passkeys.dev |

---

*Checklist này được thiết kế theo WebAuthn Level 2 và FIDO2 specification. Cập nhật định kỳ khi spec thay đổi.*