Bỏ qua để đến nội dung

Social Login (Google & Apple)

Tài liệu này tổng hợp kiến trúc xác thực liên kết (Federated Authentication) qua Google OIDC và Sign in with Apple (iCloud) trong justo-service, ghi nhận Quyết định chốt cho từng Edge Case, và liên kết tới kế hoạch triển khai chi tiết tại memory/plan/plan_social_login_hardening.md.


1. Bảng Tổng Hợp Quyết Định Chốt Cho 8 Edge Cases

Phần tiêu đề “1. Bảng Tổng Hợp Quyết Định Chốt Cho 8 Edge Cases”
# Edge Case Quyết định chốt của Dự án (Product / Architecture) Trạng thái xử lý
1 Apple chỉ trả fullName ở lần đầu & không nằm trong JWT “Không lấy name của user từ bước này. Sẽ có bước khác để lấy name của user.” Bỏ lấy FullName ở SocialLogin & Verifier; thu thập Name ở bước Onboarding riêng.
2 Apple Hide My Email (@privaterelay.appleid.com) gây trùng tài khoản “Hiện tại dự án có bước verify số điện thoại nữa rồi mới tạo account.” Không tạo User ở bước SocialLogin; chốt chặn chống trùng lặp nằm ở bước Verify Số điện thoại (Phone OTP).
3 Pre-Account Takeover & Recycled Email khi Auto-Link “Nếu đăng ký trước thì chúng tôi có verify email gửi OTP về email rồi nên cũng không thể làm thế.” Luồng Email thường đã bắt buộc Verify Email OTP + Phone OTP trước khi tạo User. Chỉ bổ sung kiểm tra chống xung đột provider_sub khi auto-link theo email.
4 Thời điểm tạo bản ghi User trong hệ thống “Đúng rồi phải verify phone thành công thì mới tạo user trong hệ thống.” Khi SocialLogin chưa tìm thấy User, chỉ lưu trạng thái xác thực Social vào phiên để đi tiếp sang Verify Phone OTP; chỉ tạo User sau khi Verify Phone thành công.
5 Đa nền tảng Client IDs (aud / azp cho iOS, Android, Web) “Ok” Hỗ trợ mảng AllowedGoogleClientIDs và AllowedAppleClientIDs trong VerifierConfig.
6 JWKS Cache Busting (Outbound DoS) & singleflight Context “Ok” Bổ sung minRefreshInterval, tách context.WithoutCancel trong singleflight, và giữ cache cũ khi lỗi mạng tạm thời.
7 Race Condition khi Burn Nonce & DB Transaction “Ok” Chuyển sang câu lệnh SQL UPDATE nguyên tử (ConsumeSocialNonce) và dùng DB Transaction khi ghi User + UserIdentity.
8 Apple Webhook (consent-revoked / account-delete) “Ok” Thu hồi phiên đăng nhập (auth_sessions) và đánh dấu revoked_at thay vì xóa cứng làm mất liên kết tài khoản.

2. Sơ đồ Luồng Xác Thực Social OIDC & Onboarding Đã Chốt

Phần tiêu đề “2. Sơ đồ Luồng Xác Thực Social OIDC & Onboarding Đã Chốt”
CLIENT (iOS / Android / Web) SERVER (justo-service)
│ │
├─ 1. POST /v1/auth/init-session (Attestation + P-256 ECDH) ─────>│
│<─── Trả về jwtToken (chứa sessionId) ───────────────────────────┤
│ │
├─ 2. POST /v1/auth/social/nonce (sessionId) ────────────────────>│
│ ├─ Sinh 32 bytes CSPRNG
│ ├─ Tính hashedNonce = SHA256_hex(raw)
│ ├─ Lưu hashedNonce vào auth_sessions (TTL 10m)
│<─── Trả về { nonce: hashedNonce, expiresAt } ───────────────────┤
│ │
├─ 3. Gọi Google Sign-In SDK / Apple ASAuthorization │
│ (truyền trực tiếp hashedNonce vào request) │
│<─── Nhận id_token (JWT do Google/Apple ký RS256, chứa nonce) ───┤
│ │
├─ 4. POST /v1/auth/social-login ───────────────────────────────>│
│ Body: { sessionId, provider, idToken, nonce } │
│ ├─ Atomic SQL ConsumeSocialNonce (chống Replay)
│ ├─ Verify JWT RS256 qua JWKS Cache (Rate-limited)
│ ├─ Kiểm tra iss, aud/azp (Multi-Platform), exp, email_verified
│ ├─ Tra cứu user_identities(provider, sub)
│ │ ├─ ĐÃ CÓ USER: Bind userId vào auth_sessions -> Login hoàn tất
│ │ └─ CHƯA CÓ USER: Lưu trạng thái Social (provider, sub, email)
│ │ vào phiên -> Chuyển sang luồng Onboarding
│ │ (Thu thập Name -> Verify Phone OTP -> Tạo User + Identity)

3. Chi Tiết Phân Tích Từng Edge Case & Quyết Định Của Dự Án

Phần tiêu đề “3. Chi Tiết Phân Tích Từng Edge Case & Quyết Định Của Dự Án”

Edge Case 1: Thông tin Họ Tên (fullName) từ Google / Apple

Phần tiêu đề “Edge Case 1: Thông tin Họ Tên (fullName) từ Google / Apple”
  • Vấn đề: id_token của Apple không chứa claim name (chỉ trả ở Client trong lần đầu tiên), và tên trên Google không đảm bảo chuẩn pháp lý.
  • Quyết định của dự án:

    “Không lấy name của user từ bước này. Sẽ có bước khác để lấy name của user.”

  • Hướng xử lý trong code:
    • Loại bỏ trường FullName khỏi FederatedClaims (internal/application/auth/ports.go) và bỏ việc đọc claims.Name trong Verifier (internal/infrastructure/security/federated/verifier.go).
    • Trong Service.SocialLogin (internal/application/auth/social_login.go), bỏ logic cắt chuỗi trước dấu @ của email để gán DisplayName. Tên người dùng sẽ được cập nhật ở bước Onboarding tiếp theo.

Edge Case 2: Apple “Hide My Email” (@privaterelay.appleid.com) & Chống trùng lặp tài khoản

Phần tiêu đề “Edge Case 2: Apple “Hide My Email” (@privaterelay.appleid.com) & Chống trùng lặp tài khoản”
  • Vấn đề: Người dùng bật tính năng Hide My Email của Apple sẽ nhận email ẩn danh @privaterelay.appleid.com. Nếu tạo ngay bản ghi users ở bước Social Login chỉ dựa trên email, một người dùng có thể tạo nhiều tài khoản trùng lặp.
  • Quyết định của dự án:

    “Hiện tại dự án có bước verify số điện thoại nữa rồi mới tạo account.”

  • Hướng xử lý trong code:
    • Không tạo bản ghi users ở bước SocialLogin.
    • Việc chống trùng lặp danh tính người dùng được kiểm soát tại bước Verify Số điện thoại (Phone OTP) trước khi khởi tạo tài khoản chính thức.

Edge Case 3: Pre-Account Takeover & Recycled Corporate Email

Phần tiêu đề “Edge Case 3: Pre-Account Takeover & Recycled Corporate Email”
  • Vấn đề:
    1. Kẻ tấn công đăng ký trước bằng email của nạn nhân (Pre-Registration Hijack).
    2. Công ty dùng Google Workspace tái sử dụng email của nhân viên cũ cho nhân viên mới (cùng email, khác provider_sub).
  • Quyết định của dự án:

    “Nếu đăng ký trước thì chúng tôi có verify email gửi otp về email rồi nên cũng không thể làm thế.”

  • Hướng xử lý trong code:
    • Do luồng đăng ký thường bắt buộc Verify Email OTP và Verify Phone OTP trước khi tạo User, kịch bản đăng ký chiếm chỗ trước đã bị triệt tiêu hoàn toàn bởi quy trình nghiệp vụ.
    • Khi auto-link vào User đã tồn tại theo email, chỉ cần kiểm tra thêm: nếu User đó đã có liên kết cùng provider nhưng khác provider_sub thì từ chối auto-link (ErrIdentityExists) để chống Recycled Corporate Email.

Edge Case 4: Thời điểm Tạo User trong Hệ thống (Sau khi Verify Phone)

Phần tiêu đề “Edge Case 4: Thời điểm Tạo User trong Hệ thống (Sau khi Verify Phone)”
  • Vấn đề: Trong Service.SocialLogin hiện tại, khi chưa tìm thấy User, code đang gọi SaveUser và SaveIdentity tạo ngay bản ghi users với Status: active dù chưa verify số điện thoại.
  • Quyết định của dự án:

    “Đúng rồi phải verify phone thành công thì mới tạo user trong hệ thống.”

  • Hướng xử lý trong code:
    • Khi SocialLogin xác thực thành công token Google/Apple nhưng chưa có User trong DB:
      1. Không tạo User và UserIdentity ngay trong SocialLogin.
      2. Lưu thông tin định danh Social đã xác thực (social_provider, social_sub, social_email) vào phiên hiện tại (auth_sessions).
      3. Trả về kết quả để Client chuyển sang màn hình thu thập thông tin và Verify Phone OTP.
      4. Khi hoàn tất Verify Phone OTP và kích hoạt tài khoản, tạo đồng thời User + UserIdentity trong cùng một Database Transaction.

Edge Case 5: Đa nền tảng (Multi-Platform Client IDs: iOS, Android, Web)

Phần tiêu đề “Edge Case 5: Đa nền tảng (Multi-Platform Client IDs: iOS, Android, Web)”
  • Quyết định của dự án: “Ok”
  • Hướng xử lý trong code:
    • Mở rộng VerifierConfig (internal/infrastructure/security/federated/verifier.go) và Config (internal/configs/config.go) để hỗ trợ danh sách GoogleClientIDs []string và AppleClientIDs []string (cho phép cấu hình nhiều Client ID qua biến môi trường phân tách bằng dấu phẩy cho iOS, Android, Web).

Edge Case 6: Chống JWKS Cache Busting (Outbound DoS) & singleflight Context Cancellation

Phần tiêu đề “Edge Case 6: Chống JWKS Cache Busting (Outbound DoS) & singleflight Context Cancellation”
  • Quyết định của dự án: “Ok”
  • Hướng xử lý trong code (internal/infrastructure/security/federated/jwks_cache.go):
    1. Thêm minRefreshInterval (mặc định 30 giây): Khi gặp kid không có trong cache, nếu mới fetch JWKS trong vòng minRefreshInterval thì trả về ErrJWKNotFound ngay mà không gọi HTTP ra ngoài tới Google/Apple.
    2. Tách context.WithoutCancel(ctx) kèm timeout độc lập bên trong sfg.Do(...) để một request bị hủy không làm hỏng các request đang chờ chung singleflight.
    3. Giữ lại tập keys cũ (Stale-While-Revalidate) nếu lần fetch làm tươi bị lỗi mạng tạm thời.

Edge Case 7: Atomic Nonce Burn & Database Transaction

Phần tiêu đề “Edge Case 7: Atomic Nonce Burn & Database Transaction”
  • Quyết định của dự án: “Ok”
  • Hướng xử lý trong code:
    1. Bổ sung phương thức ConsumeSocialNonce(ctx, sessionID, expectedNonce string, now time.Time) error trong Repository và PostgresRepository sử dụng câu lệnh UPDATE ... WHERE id = $1 AND social_nonce = $2 AND social_nonce_expires_at > $3 nguyên tử, tránh hoàn toàn Race Condition khi 2 request gửi song song.
    2. Đảm bảo thao tác lưu User + UserIdentity khi hoàn tất xác thực được thực thi trong một Database Transaction (pgx.Tx).

Phần tiêu đề “Edge Case 8: Apple Webhook (account-delete / consent-revoked)”
  • Quyết định của dự án: “Ok”
  • Hướng xử lý trong code:
    • Bổ sung cột revoked_at TIMESTAMPTZ vào bảng user_identities và phương thức thu hồi phiên theo user_id.
    • Khi nhận consent-revoked hoặc account-delete từ Apple (Service.HandleAppleNotification), đánh dấu revoked_at trên user_identities và xóa/thu hồi các phiên auth_sessions đang hoạt động của user_id đó (trong 1 Transaction), bảo toàn bản ghi users và lịch sử tài chính.

4. Liên Kết Kế Hoạch Triển Khai (Implementation Plan)

Phần tiêu đề “4. Liên Kết Kế Hoạch Triển Khai (Implementation Plan)”

Chi tiết kế hoạch thực hiện theo 3 Phase tuân thủ Onion Architecture được lưu tại:

  • Plan tổng thể: memory/plan/plan_social_login_hardening.md
    • Phase 1 (Infrastructure Security Hardening): memory/plan/plan_P1_social_infra_hardening.md
    • Phase 2 (Domain & Persistence Atomic Operations): memory/plan/plan_P2_social_persistence_atomic.md
    • Phase 3 (Application Use Cases & Onboarding Flow Alignment): memory/plan/plan_P3_social_app_onboarding_flow.md
  • Bảng theo dõi tiến độ: memory/plan/CHECKLIST.md