Zoom-in: OAuth 2.0

"Đăng nhập bằng Google" — 3 từ, một click. Bên dưới đó là một cơ chế mà password không bao giờ rời khỏi Google, app không bao giờ biết mật khẩu của người dùng, và người dùng có thể thu hồi quyền truy cập bất cứ lúc nào mà không cần đổi password.
Phóng to dần vào đó.
Layer 1 — Bài toán delegation
Cách cũ: muốn app A đọc email của người dùng trên Google → người dùng phải đưa username/password cho app A.
graph LR
U["👤 User"] -->|"username + password"| A["📱 App A"]
A -->|"login as user"| G["🔑 Google"]
style U fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style A fill:#1a3a2a,stroke:#22c55e,color:#86efac
style G fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
Vấn đề: App A có toàn quyền với tài khoản Google — không giới hạn scope, không có cách thu hồi riêng, và nếu App A bị lộ dữ liệu thì password Google của người dùng cũng bị lộ.
OAuth 2.0 giải quyết bằng một khái niệm khác: thay vì đưa chìa khóa nhà, cấp một token có giới hạn thời gian và scope.
Layer 2 — Authorization Code flow
OAuth 2.0 tách quá trình thành hai bước: lấy code → đổi code lấy token.
sequenceDiagram
participant U as 👤 User
participant A as 📱 App (Client)
participant G as 🔑 Google Auth Server
participant R as 📦 Resource Server (Gmail API)
U->>A: "Đăng nhập bằng Google"
A->>G: Redirect → /authorize?client_id=...&scope=email&redirect_uri=...&state=xyz
G-->>U: Hiện consent screen
U->>G: Đồng ý cấp quyền
G-->>A: Redirect về redirect_uri?code=AUTH_CODE&state=xyz
A->>G: POST /token (code=AUTH_CODE + client_secret)
G-->>A: access_token + refresh_token
A->>R: GET /gmail/messages (Authorization: Bearer access_token)
R-->>A: Dữ liệu email
Authorization code chỉ sống vài giây và chỉ dùng được một lần — nó đi qua browser URL, nhưng không phải token thật. Token thật (access_token) được đổi qua server-to-server với client_secret, không qua browser.
Tham số state là một chuỗi ngẫu nhiên do app tạo ra — dùng để chống CSRF: nếu response trả về state khác với state đã gửi đi, flow bị hủy.
Layer 3 — Access token vs Refresh token
Hai loại token, hai mục đích khác nhau.
graph LR
A["📱 App"] -->|"access_token (15 phút)"| R["📦 Resource Server"]
A -->|"refresh_token (30 ngày)"| G["🔑 Auth Server"]
G -->|"access_token mới"| A
style A fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style R fill:#1a3a2a,stroke:#22c55e,color:#86efac
style G fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
| Access token | Refresh token | |
|---|---|---|
| Gửi đến | Resource server (mọi request) | Auth server (khi access token hết hạn) |
| Thời hạn | 15 phút - 1 giờ | Ngày đến tháng |
| Nếu bị lộ | Nguy hiểm trong thời gian ngắn | Nguy hiểm hơn, nhưng có thể thu hồi |
Tách hai loại token giúp giảm rủi ro: access token ngắn hạn nên ngay cả khi bị lộ, window tấn công nhỏ. Refresh token chỉ đi đến auth server — ít exposure hơn.
client_secret — secret này chỉ an toàn ở server-side app. Mobile app và SPA không có nơi giữ secret an toàn.
Layer 4 — PKCE: bảo vệ public client
Mobile app và browser SPA là public client — không có server, không có nơi giữ client_secret. PKCE (Proof Key for Code Exchange) thay thế client_secret bằng một cặp giá trị tạm thời.
sequenceDiagram
participant A as 📱 Mobile App
participant G as 🔑 Auth Server
Note over A: Tạo code_verifier (ngẫu nhiên, 43-128 ký tự)
Note over A: code_challenge = SHA256(code_verifier)
A->>G: /authorize?...&code_challenge=...&code_challenge_method=S256
G-->>A: code=AUTH_CODE
A->>G: /token (code=AUTH_CODE + code_verifier)
Note over G: SHA256(code_verifier) == code_challenge? ✓
G-->>A: access_token + refresh_token
code_verifier chỉ tồn tại trong memory của app, không bao giờ rời khỏi app cho đến bước đổi token. Ngay cả khi AUTH_CODE bị chặn, kẻ tấn công không có code_verifier để đổi lấy token.
Full picture
sequenceDiagram
participant U as 👤 User
participant A as 📱 App
participant G as 🔑 Auth Server
participant R as 📦 Resource Server
Note over A: Tạo state + PKCE (nếu public client)
A->>G: /authorize (scope, redirect_uri, state, code_challenge)
G-->>U: Consent screen
U->>G: Chấp nhận
G-->>A: code + state
Note over A: Xác nhận state
A->>G: /token (code + client_secret hoặc code_verifier)
G-->>A: access_token (15ph) + refresh_token (30 ngày)
loop Mỗi request
A->>R: API call (Bearer access_token)
R-->>A: Dữ liệu
end
Note over A: access_token hết hạn
A->>G: /token (refresh_token)
G-->>A: access_token mới
Takeaway
OAuth 2.0 tách biệt ba thứ: ai là người dùng, ai được cấp quyền, và quyền cụ thể là gì. Password không rời khỏi auth server. Token có scope giới hạn và thời hạn ngắn. Refresh token cho phép duy trì session mà không hỏi lại người dùng.
Khi implement OAuth, hai điểm hay bị bỏ qua: validate state để chống CSRF, và dùng PKCE cho mọi public client — kể cả khi server hỗ trợ client_secret, vì PKCE thêm một lớp bảo vệ không có chi phí.
Bài viết được hỗ trợ bởi Amy 🌸 - AI Assistant. Nội dung đã được kiểm duyệt bởi tác giả.
Related Posts
Zoom-in: JWT
Gọi API, server cho qua. Bên trong token đó là chữ ký số — không phải mã hóa, và đó là điều quan trọng nhất cần hiểu.
Zoom-in: Asymmetric Encryption
HTTPS an toàn vì được mã hóa. Nhưng ai đã mã hóa, ai giải mã, và tại sao không ai giả mạo được server — đó mới là câu hỏi thật sự.
Zoom-in: TCP
Mọi HTTP request đều đi trên TCP — nhưng trước khi byte đầu tiên của dữ liệu đi qua, đã có 3 gói tin trao đổi mà không mang dữ liệu nào. TCP giải quyết vấn đề mà Internet không giải quyết được.