Skip to main content

Chương 7: OAuth 2.0

Khái niệm

OAuth 2.0 là framework ủy quyền (authorization framework) cho phép ứng dụng thứ ba truy cập tài nguyên của người dùng mà không cần biết password của họ.

Mức độ nguy hiểm: Cao — Misconfigured OAuth = account takeover không cần password.

Ví dụ quen thuộc: "Đăng nhập bằng Google/GitHub/Facebook". Bạn cho phép app đó đọc email/profile mà không chia sẻ password Gmail/GitHub.

Ba bên tham gia:

  • Resource Owner: User (bạn)
  • Client: Ứng dụng thứ ba muốn truy cập
  • Authorization Server / Resource Server: Google, GitHub, Facebook

OAuth 2.0 Flows

Authorization Code Flow (Phổ biến nhất, an toàn nhất)

User Client App Auth Server Resource Server
│ │ │ │
│ Click Login │ │ │
│──────────────►│ │ │
│ │──── Redirect ───►│ │
│ │ /authorize? │ │
│ │ client_id=... │ │
│ │ redirect_uri= │ │
│ │ response_type= │ │
│ │ code │ │
│ │ state=RANDOM │ │
│◄──────────────┤ │ │
│ Login page │ │ │
│──────────────────────────────────► │
│ User logins + grants consent │
│◄────────────────────────────────── │
│ Redirect to: client.com/callback?code=AUTH_CODE │
│ │ │ │
│ │── POST /token ──►│ │
│ │ code=AUTH_CODE │ │
│ │ client_secret │ │
│ │◄─ access_token ──│ │
│ │ │ │
│ │────── GET /userinfo ───────────────►│
│ │◄───── user data ────────────────────│

Key parameters:

  • client_id: ID của app đã đăng ký
  • redirect_uri: URL callback sau khi authorize
  • response_type=code: Yêu cầu authorization code
  • scope: Quyền muốn xin (email, profile, read:repo)
  • state: Random token chống CSRF

Implicit Flow (Deprecated — Không nên dùng)

Trực tiếp trả về access_token trong URL fragment:
redirect_uri?access_token=TOKEN&token_type=Bearer

→ Token bị lộ trong browser history, logs, Referer header
→ Đã bị deprecated trong OAuth 2.1

PKCE (Proof Key for Code Exchange) — Mobile/SPA

Cho native apps và SPAs không thể bảo vệ client_secret:

1. App generate:
code_verifier = random_string(43-128 chars)
code_challenge = base64url(SHA256(code_verifier))

2. /authorize request thêm:
code_challenge=<challenge>
code_challenge_method=S256

3. Token exchange thêm:
code_verifier=<verifier>
(Server verify: SHA256(verifier) == challenge)

Các lỗ hổng OAuth phổ biến

1. Missing State Parameter — CSRF Attack

State là random token được tạo bởi client, gửi kèm request, và verify khi nhận callback.

Nếu không có state parameter:

1. Hacker tạo authorization URL của hắn:
/authorize?client_id=app&redirect_uri=https://app.com/callback

2. Hacker click URL → Auth Server redirect đến hacker's URI
→ hacker nhận auth_code NHƯNG KHÔNG DÙNG

3. Hacker tạo link cho victim với auth_code của hắn:
https://app.com/callback?code=HACKER_CODE

4. Victim (đang login app.com) click link
→ app.com exchange HACKER_CODE → lấy access_token của hacker
→ Victim's app account giờ được link với hacker's social account
→ Hacker login vào app.com bằng social account của hắn
→ Hacker vào account của victim!

Phòng chống:

# 1. Generate state khi tạo authorization URL
import secrets
state = secrets.token_urlsafe(32)
session['oauth_state'] = state

auth_url = f"{AUTH_URL}?client_id={CLIENT_ID}&state={state}&..."
return redirect(auth_url)

# 2. Verify state trong callback
@app.route('/callback')
def oauth_callback():
received_state = request.args.get('state')
expected_state = session.get('oauth_state')

if not received_state or received_state != expected_state:
abort(400, "State mismatch — possible CSRF")

# Proceed with code exchange

2. Flawed redirect_uri Validation

Auth Server phải validate redirect_uri chặt chẽ. Nếu không, hacker redirect authorization code đến domain của hắn.

Đăng ký: redirect_uri=https://app.com/callback

Tấn công:
/authorize?redirect_uri=https://app.com/callback/../../../evil-path
/authorize?redirect_uri=https://app.com.attacker.com/callback
/authorize?redirect_uri=https://app.com/callback%2F%2F%40attacker.com

Sau khi user authorize:
Auth Server redirect đến attacker.com với auth code
→ Hacker exchange code → access_token của victim

Bypass kỹ thuật:

Path traversal: /callback/../leak
Open redirect: /callback?next=https://attacker.com
Subdomain: evil.app.com
Port: app.com:8080

Phòng chống: Auth Server phải dùng exact string match, không regex match.

3. Token Leakage qua Referer

Với Implicit Flow (deprecated):

access_token trong URL fragment:
https://app.com/callback#access_token=TOKEN

→ Nếu app load external resources (images, analytics):
Referer: https://app.com/callback#access_token=TOKEN
→ Token bị gửi đến external servers trong Referer header

4. Improper Token Validation

# SAI: Trust access_token payload mà không verify với Auth Server
def login_with_oauth():
token = request.headers.get('Authorization').split(' ')[1]
# Decode JWT token
payload = jwt.decode(token, verify=False) # ← NGUY HIỂM!
user_id = payload['sub']
return login_as(user_id)

# ĐÚNG: Verify token với Auth Server
def login_with_oauth():
token = request.args.get('code')
# Exchange code cho token
token_response = requests.post(TOKEN_URL, data={
'code': token,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'redirect_uri': REDIRECT_URI,
})
access_token = token_response.json()['access_token']

# Verify với Auth Server để lấy user info
user_info = requests.get(USERINFO_URL, headers={
'Authorization': f'Bearer {access_token}'
}).json()

return login_as(user_info['sub'])

5. Scope Escalation

Trong Implicit Flow:
/authorize?scope=profile

→ Sau khi nhận token, hacker modify request:
GET /api/emails HTTP/1.1
Authorization: Bearer <token>
X-OAuth-Scope: email ← thêm scope

→ Nếu Resource Server không verify scope từ Auth Server
→ Hacker truy cập email dù chỉ request scope=profile

6. Account Takeover qua Email không được verify

Scenario:
1. Hacker register social account với email: victim@example.com
(Google/GitHub không verify email ngay lập tức)
2. Hacker OAuth login vào app bằng social account này
3. App match bằng email: victim@example.com → login vào account victim

→ Hacker chiếm quyền account mà không cần password

Phòng chống: Không dùng email làm unique identifier cho OAuth login. Dùng provider + provider_user_id.


Kịch bản tấn công: redirect_uri bypass

Target: App đăng ký redirect_uri=https://example.com/callback

1. App có open redirect:
GET /redirect?url=https://attacker.com → 302 to attacker.com

2. Hacker craft URL:
/authorize?
client_id=CLIENT_ID
&redirect_uri=https://example.com/redirect?url=https://attacker.com
&response_type=code
&scope=email

3. Auth Server validate: https://example.com/redirect... ✓ (domain match)
4. User authorize
5. Auth Server redirect đến:
https://example.com/redirect?url=https://attacker.com&code=AUTH_CODE
6. App's open redirect sends user to:
https://attacker.com?code=AUTH_CODE

7. Hacker exchange code:
POST /token?code=AUTH_CODE&client_id=...&client_secret=...
→ access_token của victim!

Cách phát hiện

□ Không có state parameter trong authorization URL
□ State parameter predictable hoặc không verified
□ redirect_uri được accept khi thêm path traversal sequences
□ Auth Server accept partial domain match
□ Token trong URL (Implicit Flow)
□ App không verify token với Auth Server (chỉ decode JWT)
□ Email được dùng làm identifier mà không check verified status
□ Authorization code có thể reuse nhiều lần
□ Thiếu PKCE với native/SPA apps

Phòng chống

# Complete OAuth implementation với security best practices

class OAuthClient:
def start_login(self):
# Generate và lưu state
state = secrets.token_urlsafe(32)
# Generate PKCE (nếu là public client)
code_verifier = secrets.token_urlsafe(64)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()

session['oauth_state'] = state
session['code_verifier'] = code_verifier

params = {
'client_id': CLIENT_ID,
'redirect_uri': REDIRECT_URI, # Hardcoded, không từ user input
'response_type': 'code',
'scope': 'openid email profile',
'state': state,
'code_challenge': code_challenge,
'code_challenge_method': 'S256',
}
return redirect(f"{AUTH_URL}?" + urlencode(params))

def handle_callback(self):
# 1. Verify state
if request.args.get('state') != session.pop('oauth_state', None):
abort(400)

# 2. Exchange code
code = request.args.get('code')
token_response = requests.post(TOKEN_URL, data={
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': REDIRECT_URI,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'code_verifier': session.pop('code_verifier', ''),
})

if token_response.status_code != 200:
abort(401)

tokens = token_response.json()

# 3. Verify với UserInfo endpoint
user_info = requests.get(USERINFO_URL, headers={
'Authorization': f"Bearer {tokens['access_token']}"
}).json()

# 4. Link bằng provider + sub (không phải email)
user = User.find_or_create(
provider='google',
provider_user_id=user_info['sub'], # Unique ID của provider
email=user_info['email'],
email_verified=user_info.get('email_verified', False)
)

if not user.email_verified:
abort(403, "Email not verified with provider")

session['user_id'] = user.id
return redirect('/dashboard')

Góc nhìn DevOps

OAuth với Kubernetes — Dex / OAuth2 Proxy:

# oauth2-proxy cho Kubernetes Ingress
apiVersion: v1
kind: ConfigMap
metadata:
name: oauth2-proxy-config
data:
config: |
provider = "github"
client-id = "..."
client-secret = "..."
# Restrict to specific GitHub org
github-org = "mycompany"
cookie-secure = true
cookie-httponly = true
cookie-samesite = "lax"

Secrets cho OAuth trong K8s:

# Không hardcode client_secret trong config files
kubectl create secret generic oauth-credentials \
--from-literal=client-id=xxx \
--from-literal=client-secret=yyy \
--namespace production

Tóm tắt

  • OAuth 2.0 là về authorization, không phải authentication — OpenID Connect (OIDC) thêm authentication layer.
  • Authorization Code Flow + PKCE là secure nhất — dùng cho mọi app mới.
  • Implicit Flow đã deprecated — tránh hoàn toàn.
  • State parameter bắt buộc để chống CSRF.
  • redirect_uri phải exact match — không regex, không partial.
  • Không dùng email làm identifier vì có thể chưa verified.
  • Verify token với Auth Server — đừng chỉ decode JWT locally.

Câu hỏi ôn tập

  1. Sự khác nhau giữa Authorization Code Flow và Implicit Flow là gì? Tại sao Implicit Flow deprecated?
  2. State parameter trong OAuth bảo vệ chống loại tấn công nào? Mô tả attack scenario.
  3. Tại sao không nên dùng email làm identifier khi implement OAuth login?
  4. PKCE là gì và tại sao cần thiết cho mobile/SPA apps?
  5. Nếu Auth Server dùng partial domain match cho redirect_uri validation, hacker có thể exploit như thế nào?