# 🔵 Đăng nhập bằng Google: OAuth 2.0 ở mức thực dụng

By Better Wise

Bài này mở đầu module cuối, sau khi bạn đã có session/JWT, refresh token rotation và RBAC ở tay. Đăng nhập Google không thay thế mấy thứ đó: nó chỉ là một cách *chứng minh danh tính* người dùng, xong rồi bạn vẫn phát session hoặc JWT của chính hệ thống mình. Suốt bài, mình sẽ hỏi đúng ba câu mà mình hay hỏi khi debug: **token này ai giữ, sống bao lâu, bị lộ thì sao?**

Sau bài này bạn sẽ:

- Tạo được OAuth client với redirect URI và scope đúng
- Vẽ lại được luồng Authorization Code + PKCE và biết code đổi ở đâu
- Kiểm tra id_token trước khi tin bất kỳ claim nào
- Map `sub` của Google về user trong database, xử lý trùng email an toàn

## Bốn vai trong luồng, ai giữ cái gì

Đừng học thuộc lòng OAuth. Chỉ cần nhớ bốn chỗ:

1. **Người dùng**: bấm "Đăng nhập bằng Google", gõ mật khẩu ở trang của Google, không phải trang của bạn.
2. **Frontend của bạn**: chỉ giữ `Client ID`, `redirect_uri`, `state`, `code_verifier` tạm thời.
3. **Google**: phát `authorization code`, sau đó phát `id_token` và `access_token`.
4. **Backend của bạn**: giữ `Client Secret`, đổi code lấy token, verify, rồi cấp phiên đăng nhập của riêng bạn.

```mermaid
flowchart TD
  U["🧑 Người dùng"] --> F["🔐 Frontend"]
  F --> G["🔑 Google OAuth"]
  G --> C["📥 Callback"]
  C --> B["🖥️ Backend: đổi code và verify"]
  B --> D["🗄️ users + oauth_accounts"]
  D --> S["🎫 Session/JWT của bạn"]
```

Điểm mấu chốt: **authorization code đổi lấy token phải xảy ra ở backend**. Nếu bạn thấy code chạy kiểu `fetch` thẳng tới `oauth2.googleapis.com/token` từ trình duyệt kèm `client_secret`, đó là chỗ rò rỉ.

## Tạo OAuth client: ba giá trị và một sai lầm

Trong Google Cloud Console bạn khai báo:

| Giá trị | Được lộ ra client? | Ghi chú |
| --- | --- | --- |
| Client ID | Có | Nằm trong URL authorization |
| Client Secret | **Không** | Chỉ ở backend, không nhét vào biến môi trường của frontend |
| Authorized redirect URI | Có | Phải khớp chính xác từng ký tự, không wildcard |

Scope tối thiểu cho đăng nhập là `openid email profile`. Xin thêm scope là xin thêm quyền đọc dữ liệu người ta, hãy xin đúng cái mình dùng.

> **Warning:** 
> `redirect_uri` dùng wildcard hoặc nhận URL bất kỳ từ query là lỗ hổng kinh điển: kẻ tấn công thay `redirect_uri` trỏ về domain của hắn để hứng code. Luôn so khớp với một danh sách trắng cố định.

**Bài tập 1.** Bạn cần đưa thông tin OAuth client vào code frontend. Giá trị nào được phép nằm trong bundle JS, giá trị nào tuyệt đối không?

**Gợi ý đáp án**

`Client ID` được phép (nó vốn công khai trong URL). `Client Secret` không được, chỉ backend giữ. `redirect_uri` phải khai báo cố định ở cả hai phía.

## Luồng Authorization Code + PKCE

Với web app có backend, luồng đúng là:

1. Frontend tạo `state` ngẫu nhiên và `code_verifier`, lưu tạm ở sessionStorage.
2. Chuyển hướng tới `accounts.google.com/o/oauth2/v2/auth` kèm `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge`.
3. Người dùng đồng ý, Google redirect về `/auth/google/callback?code=...&state=...`.
4. Backend kiểm tra `state` khớp, rồi POST `code` + `code_verifier` + `client_secret` lên token endpoint.
5. Google trả `id_token` (JWT) và `access_token`. Backend verify `id_token`.
6. Backend map `sub` về user, tạo session/JWT của bạn, redirect về app.

Tham số `state` dùng để chống _____ trên luồng OAuth: nếu không kiểm tra, kẻ tấn công có thể ép trình duyệt nạn nhân hoàn tất đăng nhập vào tài khoản của kẻ đó (login CSRF).

## Verify id_token trước khi tin bất cứ thứ gì

Frontend có thể gửi lên bất cứ payload nào. Chỉ `id_token` do backend nhận trực tiếp từ Google (qua kênh đổi code) mới đáng tin. Tối thiểu kiểm tra:

- `iss` là `accounts.google.com` hoặc `https://accounts.google.com`
- `aud` khớp đúng Client ID của bạn
- `exp` chưa hết hạn
- `nonce` khớp nếu bạn có gửi
- chữ ký hợp lệ theo JWKS của Google, hoặc dùng thư viện chính thức của Google để verify

Claim _____ là định danh ổn định của người dùng Google. Đừng dùng email làm khóa chính: người ta có thể đổi email, còn `sub` thì không. Và đừng quên: frontend gửi email lên API là **không đủ** để đăng nhập.

**Bài tập 2.** Backend nhận `id_token` có payload: `iss = accounts.google.com`, `aud = 123-xyz.apps.googleusercontent.com` (khác Client ID của bạn), `exp` là 10 phút trước, `email_verified = true`. Xử lý thế nào?

**Gợi ý đáp án**

Từ chối. `aud` sai nghĩa là token phát cho ứng dụng khác, `exp` đã hết hạn. Chỉ cần một điều kiện sai là trả 401, không cố "nới lỏng cho dễ test".

## Map tài khoản Google về user trong database

Tách danh tính khỏi tài khoản. Bảng `oauth_accounts` giữ liên kết:

| Cột | Ví dụ | Ghi chú |
| --- | --- | --- |
| provider | google | |
| provider_user_id | 110234... | chính là claim `sub`, unique cùng provider |
| user_id | 42 | FK tới `users` |
| email_at_provider | an@gmail.com | chỉ để hiển thị, không dùng làm khóa |

Luồng xử lý an toàn:

1. Tìm `(provider='google', provider_user_id=sub)`. Có thì đăng nhập user đó.
2. Chưa có thì tìm `users.email`. Nếu trùng, **chỉ tự liên kết khi email đã được verify**, và tốt nhất vẫn yêu cầu thêm một bước xác nhận (đăng nhập mật khẩu một lần, hoặc bấm link trong email).
3. Chưa có gì thì tạo `users` mới, rồi tạo dòng `oauth_accounts`.

Sau bước này bạn cấp refresh token của chính mình, có rotation và thu hồi như module 2. Token Google không phải token phiên của bạn.

**Bài tập 3.** Một người đăng nhập Google bằng email trùng với tài khoản email/mật khẩu đã tồn tại trong hệ thống. Bạn viết gì trong code?

**Gợi ý đáp án**

Không auto-link mù. Nếu `email_verified = true`: yêu cầu người dùng xác nhận quyền sở hữu (nhập mật khẩu hiện tại hoặc bấm link xác nhận qua email) rồi mới gắn `oauth_accounts`. Nếu `email_verified = false`: từ chối liên kết và coi như tài khoản mới.

## Năm vết xe đổ khi gắn Google Login

- **Đổi code ở frontend**: secret lộ, code bị dùng lại. Code đổi một lần, và phải ở server.
- **Tin email từ client**: ai cũng POST được `{"email":"admin@..."}`. Chỉ tin `id_token` đã verify.
- **Bỏ qua `state`**: login CSRF, người dùng lặng lẽ đăng nhập vào tài khoản của kẻ khác.
- **Log nguyên `id_token` và `code`**: log là nơi rò rỉ chậm mà dai. Cắt bớt hoặc băm trước khi log.
- **Cho `redirect_uri` nhận tham số động**: mở toang cửa cho kẻ hứng code.

Mẹo của mình: viết một hàm `verifyGoogleIdToken(token)` duy nhất, mọi chỗ cần đăng nhập Google đều đi qua đó, và viết test cho ba case sai (`aud`, `exp`, chữ ký). Sau này AI có sinh thêm code auth, bạn vẫn có một chốt chặn để đối chiếu.

## Chốt lại và sang bài kiểm tra code AI

Bốn điều cần mang theo: secret ở backend, code đổi ở backend, `id_token` phải verify `iss`/`aud`/`exp`/chữ ký, và `sub` là khóa map về user chứ không phải email. Luồng đăng nhập Google của bạn giờ đã đủ để ghép vào sản phẩm.

Bài tiếp theo sẽ rất thực dụng: khi bạn để AI viết luồng auth này, những sai lầm nào hay xuất hiện và checklist nào để bắt chúng trước khi phát hành.

Backend phải kiểm tra claim _____ khớp Client ID của mình và _____ chưa hết hạn trước khi tin id_token. Giá trị _____ chỉ được nằm ở backend, không bao giờ đưa vào bundle frontend.

Front: Trong OAuth client của Google, giá trị nào được phép xuất hiện ở frontend?
Back: Client ID; Client Secret chỉ nằm ở backend.

Front: Authorization code dùng để làm gì và đổi ở đâu?
Back: Đổi một lần lấy token, tại backend, kèm redirect_uri và client_secret hoặc code_verifier.

Front: Claim nào là định danh ổn định của người dùng Google?
Back: sub, không phải email.

Front: Bốn thứ tối thiểu phải kiểm tra trong id_token?
Back: iss, aud, exp và chữ ký theo JWKS của Google.

Front: Tham số state chống lại kiểu tấn công nào?
Back: CSRF trên luồng OAuth, tức login CSRF.

Front: Frontend gửi email đã đăng nhập Google lên API, backend nên làm gì?
Back: Không tin; chỉ tin id_token do backend tự đổi code và verify.

Front: Khóa duy nhất của bảng oauth_accounts là gì?
Back: Cặp (provider, provider_user_id), ví dụ ('google', sub).

Front: Có nên tự động liên kết Google vào user cùng email?
Back: Không auto-link mù; chỉ liên kết khi email đã verify và có bước xác nhận.

Front: Sau khi đăng nhập Google xong, app cấp gì cho client?
Back: Session hoặc JWT kèm refresh token của chính hệ thống bạn.

Front: redirect_uri phải được cấu hình thế nào?
Back: Khớp chính xác với danh sách trắng đã khai báo, không wildcard, không nhận URL động.

Front: Scope tối thiểu cho đăng nhập Google?
Back: openid email profile.

Front: Vì sao không dùng email làm khóa chính của user Google?
Back: Email có thể đổi; sub thì ổn định và gắn với tài khoản Google.

Front: PKCE dùng để làm gì?
Back: Ràng buộc code với đúng client đã bắt đầu luồng, chống đánh tráo code.

Front: Sai lầm hay gặp nhất khi gắn Google Login là gì?
Back: Đổi code hoặc giữ client_secret ở frontend, và tin email do client gửi lên.

---

## Contents

Part of [🔐 Xác thực và phân quyền: session, JWT và OAuth](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth.md)

- **🍪 Nền tảng xác thực: session, cookie và mật khẩu**
  - [🍪 Session và cookie: chọn khi nào](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth/session-va-cookie-chon-khi-nao.md)
  - [🎫 JWT: cấu trúc, cách kiểm tra và các lỗi hay gặp](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth/jwt-cau-truc-cach-kiem-tra-va-loi-hay-gap.md) (sign-in required)
  - [🔑 Hash mật khẩu: bcrypt, argon2 và lỗi thường gặp](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth/hash-mat-khau-bcrypt-argon2-va-loi-thuong-gap.md)
  - [✅ Checkpoint: nền tảng xác thực](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth/checkpoint-nen-tang-xac-thuc.md)
- **♻️ Refresh token và phân quyền theo vai trò**
  - [🔄 Refresh token: luồng cấp mới, rotation và thu hồi](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth/refresh-token-luong-cap-moi-rotation-va-thu-hoi.md)
  - [🛡️ Phân quyền theo vai trò (RBAC) trong API](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth/phan-quyen-theo-vai-tro-rbac-trong-api.md)
  - [✅ Checkpoint: refresh token và phân quyền](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth/checkpoint-refresh-token-va-phan-quyen.md)
- **🌐 Đăng nhập Google, AI viết auth và cách kiểm tra**
  - [🔵 Đăng nhập bằng Google: OAuth 2.0 ở mức thực dụng](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth/dang-nhap-bang-google-oauth-2-thuc-dung.md) — this lesson
  - [🤖 Khi để AI viết luồng auth: sai lầm và checklist kiểm tra](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth/ai-viet-luong-auth-sai-lam-va-checklist-kiem-tra.md)
  - [🧩 Mini project: ghép luồng auth hoàn chỉnh cho sản phẩm](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth/mini-project-ghep-luong-auth-hoan-chinh.md) (premium content)
  - [✅ Checkpoint: đăng nhập Google và kiểm tra luồng auth](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth/checkpoint-dang-nhap-google-va-kiem-tra-luong-auth.md) (premium content)
- [Course cheatsheet](https://everwise.study/courses/xac-thuc-va-phan-quyen-session-jwt-va-oauth/cheatsheet.md) (premium content)
