OAuth2 là gì? Hiểu đúng về Authorization Framework

OAuth2 không phải là authentication — đây là lỗi mà rất nhiều developer mắc phải. Bài viết giải thích OAuth2 từ gốc rễ: flow, grant types, token và cách áp dụng đúng trong thực tế.

Nghe bài viết này dưới dạng podcast

OAuth2 là gì? Hiểu đúng về Authorization Framework phổ biến nhất

Bạn đã bao giờ click "Đăng nhập bằng Google" rồi tự hỏi chuyện gì xảy ra phía sau không? Câu trả lời là OAuth2 — nhưng hầu hết developer hiểu sai về nó ngay từ đầu.

OAuth2 không phải là Authentication

Đây là lỗi conceptual phổ biến nhất. OAuth2 là một Authorization Framework — tức là nó xử lý việc cấp quyền truy cập, không phải xác thực danh tính.

Nói nôm na:

  • Authentication (Authn): "Bạn là ai?" — xác minh danh tính
  • Authorization (Authz): "Bạn được làm gì?" — cấp quyền truy cập

OAuth2 trả lời câu hỏi thứ hai. Nếu bạn muốn authentication đi kèm, bạn cần thêm OpenID Connect (OIDC) — một layer chạy trên top của OAuth2.

Vấn đề OAuth2 giải quyết

Trước OAuth2, kịch bản phổ biến là thế này: bạn muốn một app thứ ba (ví dụ ứng dụng quản lý email) truy cập Gmail của bạn. Cách duy nhất là... đưa username/password Gmail cho app đó.

Vấn đề hiển nhiên:

  • App có toàn quyền với tài khoản của bạn
  • Bạn không thể thu hồi quyền truy cập mà không đổi password
  • Nếu app bị hack, credential của bạn bị lộ

OAuth2 giải quyết bằng cách tạo ra access token có phạm vi (scope) giới hạn và thời hạn rõ ràng. App thứ ba không bao giờ biết password của bạn.

Các thành phần trong OAuth2

OAuth2 định nghĩa 4 vai trò:

Vai trò Mô tả Ví dụ
Resource Owner Người dùng, chủ sở hữu dữ liệu Bạn
Client App muốn truy cập dữ liệu Ứng dụng quản lý lịch
Authorization Server Cấp phát token Google Auth
Resource Server Nơi chứa dữ liệu thực Google Calendar API

Trong nhiều hệ thống, Authorization Server và Resource Server là một — nhưng về mặt conceptual chúng vẫn là hai vai trò riêng biệt.

Authorization Code Flow — Flow quan trọng nhất

Đây là flow phổ biến và an toàn nhất, dùng cho web app và mobile app. Hiểu flow này là nền tảng để hiểu phần còn lại.

User         Client App        Auth Server       Resource Server
 |               |                  |                   |
 |  1. Click login|                  |                   |
 |--------------->|                  |                   |
 |               | 2. Redirect to   |                   |
 |               |    /authorize    |                   |
 |               |----------------->|                   |
 |  3. Login & consent screen       |                   |
 |<---------------------------------|                   |
 |  4. Approve                      |                   |
 |--------------------------------->|                   |
 |               |  5. Authorization code              |
 |               |<-----------------|                   |
 |               | 6. Exchange code |                   |
 |               |    for tokens    |                   |
 |               |----------------->|                   |
 |               |  7. Access Token + Refresh Token    |
 |               |<-----------------|                   |
 |               | 8. Call API with |                   |
 |               |    Bearer token  |                   |
 |               |---------------------------------------->|
 |               |  9. Protected resource              |
 |               |<----------------------------------------|

Tại sao cần authorization code? Tại sao không trả thẳng token về URL redirect?

Vì URL có thể bị lưu trong browser history, server log, hoặc bị sniff. Authorization code là một mã tạm thời, chỉ dùng một lần, và được exchange qua back-channel (server-to-server) — an toàn hơn nhiều.

PKCE — Bắt buộc với Public Clients

Với Single Page App (SPA) hoặc mobile app — gọi là public clients vì không thể giữ bí mật client secret — bạn cần thêm PKCE (Proof Key for Code Exchange).

// 1. Generate code verifier (random string)
const codeVerifier = generateRandomString(64);

// 2. Hash it to get code challenge
const codeChallenge = base64url(sha256(codeVerifier));

// 3. Send code_challenge khi redirect đến auth server
const authUrl = `https://auth.example.com/authorize?
  client_id=YOUR_CLIENT_ID&
  response_type=code&
  redirect_uri=https://app.example.com/callback&
  code_challenge=${codeChallenge}&
  code_challenge_method=S256&
  scope=read:profile`;

// 4. Khi exchange code, gửi kèm code_verifier gốc
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: 'https://app.example.com/callback',
    client_id: 'YOUR_CLIENT_ID',
    code_verifier: codeVerifier, // Auth server sẽ verify hash này
  }),
});

Auth server sẽ hash code_verifier và so sánh với code_challenge đã nhận trước đó. Nếu ai đó intercept authorization code, họ không có code_verifier để exchange.

Các Grant Types khác

OAuth2 định nghĩa nhiều grant type cho các use case khác nhau:

Client Credentials Grant — Machine-to-Machine

Dùng khi không có user — ví dụ một microservice cần gọi API của service khác.

const response = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: 'SERVICE_CLIENT_ID',
    client_secret: 'SERVICE_CLIENT_SECRET',
    scope: 'internal:read',
  }),
});

const { access_token } = await response.json();

Đây là grant type phù hợp nhất cho service-to-service authentication trong microservices architecture.

Device Authorization Grant — Thiết bị không có trình duyệt

Dùng cho Smart TV, CLI tool, hoặc thiết bị IoT không có input device đầy đủ.

Thiết bị hiển thị: "Truy cập example.com/activate và nhập code: ABCD-1234"
User mở điện thoại, đăng nhập, nhập code
Thiết bị poll auth server cho đến khi nhận được token

Implicit Grant — Đã deprecated

Từng dùng cho SPA, nhưng nay đã bị loại bỏ vì không an toàn. Dùng Authorization Code + PKCE thay thế.

Access Token vs Refresh Token

OAuth2 thường cấp phát hai loại token:

Access Token:

  • Thời hạn ngắn (thường 15 phút đến 1 giờ)
  • Gửi kèm mỗi request đến Resource Server
  • Nếu bị lộ, thiệt hại giới hạn trong thời gian ngắn

Refresh Token:

  • Thời hạn dài (ngày, tuần, hoặc cho đến khi bị revoke)
  • Chỉ gửi đến Authorization Server để lấy Access Token mới
  • Không bao giờ gửi đến Resource Server
// Access token hết hạn — dùng refresh token
async function refreshAccessToken(refreshToken: string) {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: 'YOUR_CLIENT_ID',
    }),
  });

  return response.json();
  // Trả về access_token mới, và có thể refresh_token mới (rotation)
}

Một số auth server triển khai refresh token rotation — mỗi lần dùng refresh token sẽ cấp token mới và vô hiệu hóa token cũ. Phát hiện reuse là dấu hiệu token bị đánh cắp.

Scope — Kiểm soát Quyền Truy Cập

Scope là cơ chế để user kiểm soát chính xác app được làm gì với dữ liệu của họ.

scope=read:email read:profile
scope=repo:read org:read
scope=openid profile email  // OpenID Connect scopes

Khi thiết kế scope cho API của mình, nguyên tắc là least privilege — chỉ yêu cầu scope tối thiểu cần thiết. User sẽ tin tưởng app hơn nếu không thấy app yêu cầu quyền không liên quan.

JWT Access Token — Stateless vs Opaque Token

Access token có thể là hai dạng:

Opaque Token: Một chuỗi ngẫu nhiên, Resource Server phải gọi về Authorization Server để validate (token introspection).

JWT (JSON Web Token): Token self-contained, Resource Server có thể validate cục bộ bằng public key.

// Validate JWT tại Resource Server — không cần gọi Auth Server
import jwt from 'jsonwebtoken';

function validateToken(token: string): TokenPayload {
  try {
    const payload = jwt.verify(token, PUBLIC_KEY, {
      algorithms: ['RS256'],
      audience: 'https://api.example.com',
      issuer: 'https://auth.example.com',
    });
    return payload as TokenPayload;
  } catch (error) {
    throw new UnauthorizedError('Invalid token');
  }
}

JWT nhanh hơn vì không cần network call, nhưng không thể revoke ngay lập tức trước khi hết hạn — đây là trade-off cần cân nhắc tùy use case.

OAuth2 trong Banking — Góc nhìn Thực Chiến

Trong môi trường banking và fintech, OAuth2 được sử dụng với các yêu cầu nghiêm ngặt hơn:

  • Token lifetime ngắn hơn: Access token thường chỉ 5-15 phút
  • Proof of Possession (DPoP): Bind token với private key của client — token bị đánh cắp không dùng được từ thiết bị khác
  • Rich Authorization Requests (RAR): Scope chi tiết hơn, ví dụ chỉ cho phép chuyển khoản đến tài khoản cụ thể, tối đa số tiền nhất định
  • Strong Customer Authentication (SCA): Kết hợp với MFA theo chuẩn PSD2

Những Lỗi Phổ Biến

1. Lưu access token trong localStorage

localStorage dễ bị XSS. Dùng httpOnly cookie hoặc lưu trong memory (với SPA).

2. Không validate state parameter

state là parameter ngẫu nhiên gửi kèm authorization request và verify khi nhận callback — chống CSRF attack.

// Trước khi redirect
const state = generateRandomString(32);
sessionStorage.setItem('oauth_state', state);

// Sau khi nhận callback
const returnedState = searchParams.get('state');
const savedState = sessionStorage.getItem('oauth_state');
if (returnedState !== savedState) {
  throw new Error('State mismatch — possible CSRF attack');
}

3. Dùng Implicit Flow với SPA mới

Implicit Flow đã deprecated từ OAuth 2.1. Dùng Authorization Code + PKCE.

4. Không handle token expiry gracefully

Implement interceptor để tự động refresh token khi nhận 401 Unauthorized, thay vì để user bị đăng xuất giữa chừng.

Tóm lược OAuth2 trong một sơ đồ

OAuth2 Grant Types
│
├── Authorization Code + PKCE  → Web app, Mobile app, SPA
├── Client Credentials         → Service-to-service, M2M
├── Device Authorization       → Smart TV, CLI, IoT
└── Refresh Token              → Lấy access token mới (không phải grant type độc lập)

Deprecated (không dùng):
├── Implicit Flow
└── Resource Owner Password Credentials (ROPC)

Kết luận

OAuth2 là nền tảng của hầu hết hệ thống authorization hiện đại, nhưng nó không phải là magic button. Để dùng đúng:

  1. Hiểu đúng OAuth2 là authorization, không phải authentication — muốn authentication thêm OpenID Connect
  2. Chọn đúng grant type — Authorization Code + PKCE cho user-facing app, Client Credentials cho M2M
  3. Không bao giờ dùng Implicit Flow với ứng dụng mới
  4. Validate state parameter để chống CSRF
  5. Token lifetime ngắn + refresh token rotation để giảm blast radius nếu token bị lộ

OAuth2 có vẻ phức tạp lúc đầu, nhưng khi nắm được các thành phần và flow cốt lõi, bạn sẽ thấy nó được thiết kế khá elegant cho bài toán nó giải quyết.

Chưa có bình luận

Để lại bình luận

Bình luận sẽ được phê duyệt trước khi hiển thị.

Nhận bài viết mới

Mình sẽ gửi email khi có bài mới. Không spam.