OAuth 2.0 認證機制
Authorization Code Flow、OIDC、PKCE 與 Token 安全管理完整解析
Overview##
傳統的登入方式是使用者在你的網站輸入帳號密碼,你把密碼 hash 後存進資料庫。這個模式有幾個根本問題:
- 使用者要為每個網站記住不同的密碼
- 你要負責密碼的儲存和安全(被攻破就是你的責任)
- 沒有標準化的方式讓第三方應用存取使用者資料
OAuth 2.0 解決了這些問題——它讓使用者透過 Google、Apple 等已信任的服務登入你的應用,你不需要碰到使用者的密碼。
| 面向 | 傳統登入 | OAuth 2.0 社群登入 |
|---|---|---|
| 密碼管理 | 你負責儲存和保護 | Provider 負責(Google、Apple) |
| 使用者體驗 | 填表 + 記密碼 | 一鍵登入 |
| 信任程度 | 使用者要信任你的安全 | 使用者信任 Google/Apple |
| 開發成本 | 實作註冊/登入/忘記密碼 | 接 API + 處理 token |
Note
OAuth 2.0 是授權(Authorization)協定,不是認證(Authentication)協定。認證的部分由 OpenID Connect(OIDC) 補充——它建立在 OAuth 2.0 之上,加入了身份驗證的標準。
Architecture##
OAuth 2.0 角色###
OAuth 2.0 定義了四個角色:
| 角色 | 說明 | 實際對應 |
|---|---|---|
| Resource Owner | 資源擁有者(使用者) | 使用者本人 |
| Client | 要存取資源的應用程式 | 你的 Web App |
| Authorization Server | 發放 token 的伺服器 | Google OAuth Server |
| Resource Server | 存放資源的伺服器 | Google User Info API |
Authorization Code Flow###
這是最安全、最常用的 OAuth 2.0 流程——適用於有後端伺服器的應用:
sequenceDiagram
participant U as User
participant C as Your App
participant A as Auth Server
participant R as Resource Server
U->>C: Click "Login with Google"
C->>A: Redirect to /authorize
A->>U: Show consent screen
U->>A: Grant permission
A->>C: Redirect with auth code
C->>A: Exchange code for tokens
A->>C: Return access_token + id_token
C->>R: Request user info with access_token
R->>C: Return user profile
C->>U: Login success
步驟拆解:
Step 1 — 導向授權頁面
GET https://accounts.google.com/o/oauth2/v2/auth
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&response_type=code
&scope=openid email profile
&state=random_csrf_token
&nonce=random_nonce
| 參數 | 說明 |
|---|---|
client_id | 應用程式 ID(在 Google Console 申請) |
redirect_uri | 授權後的回調 URL(必須預先註冊) |
response_type | code 表示使用 Authorization Code Flow |
scope | 請求的權限(openid 啟用 OIDC) |
state | CSRF 防護用的隨機字串 |
nonce | 防止 replay attack 的隨機字串 |
Step 2 — 使用者同意授權
使用者在 Google 的頁面看到 consent screen,確認要授予你的應用哪些權限。
Step 3 — 取得 Authorization Code
使用者同意後,Google redirect 回你的應用,URL 帶有 code:
GET https://yourapp.com/callback
?code=AUTHORIZATION_CODE
&state=random_csrf_token
Step 4 — 用 Code 換 Token
後端用 authorization code 換取 token(這個請求在 server-side 執行,不暴露 client secret):
// Server-side: exchange code for tokens
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code: authorizationCode,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: 'https://yourapp.com/callback',
grant_type: 'authorization_code',
}),
});
const tokens = await response.json();
// tokens.access_token — 存取資源用
// tokens.id_token — 使用者身份資訊(JWT)
// tokens.refresh_token — 用來更新 access_token
Warning
Authorization Code 只能使用一次,且有效期很短(通常 10 分鐘)。client_secret 絕對不能暴露在前端程式碼中。
Token 類型###
OAuth 2.0 涉及三種 token:
| Token | 用途 | 有效期 | 存放位置 |
|---|---|---|---|
| Authorization Code | 換取 access token | 數分鐘 | URL parameter(一次性) |
| Access Token | 存取 API 資源 | 1 小時(通常) | Server memory / secure cookie |
| Refresh Token | 更新 access token | 數天 ~ 永久 | Server-side 資料庫 |
Token 生命週期:
Auth Code ──(exchange)──> Access Token ──(expire)──> 用 Refresh Token 更新
(一次性) (短期) (長期)
OIDC##
OAuth 2.0 vs OIDC###
OAuth 2.0 只負責授權(讓你的 app 存取使用者的 Google 資料),但不告訴你「這個使用者是誰」。OpenID Connect(OIDC) 在 OAuth 2.0 之上加入身份驗證:
| 面向 | OAuth 2.0 | OIDC |
|---|---|---|
| 目的 | 授權(Authorization) | 認證(Authentication) |
| 回答的問題 | 「這個 app 能存取什麼?」 | 「這個使用者是誰?」 |
| Token | Access Token | Access Token + ID Token |
| Scope | 自定義 | 必須包含 openid |
ID Token(JWT)###
OIDC 的核心是 ID Token——一個 JWT(JSON Web Token),包含使用者的身份資訊:
eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImVtYWlsIjoiam9obkBnbWFpbC5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3MTYwMDAwMDAsImV4cCI6MTcxNjAwMzYwMH0.signature
Header: { "alg": "RS256" }
Payload: {
"iss": "accounts.google.com", // 簽發者
"sub": "1234567890", // 使用者唯一 ID
"email": "john@gmail.com",
"name": "John Doe",
"iat": 1716000000, // 簽發時間
"exp": 1716003600, // 過期時間
"nonce": "random_nonce" // 防 replay
}
Signature: RS256(header + payload, private_key)
| 欄位 | 說明 |
|---|---|
iss | 簽發者(用來驗證 token 來源) |
sub | 使用者的唯一 ID(不會變) |
email | 使用者 email |
iat / exp | 簽發和過期時間 |
nonce | 對應授權請求中的 nonce,防止 replay |
Tip
sub(Subject)是使用者的唯一識別碼,不會因為改名或改 email 而變動。在你的資料庫中,用 sub 而非 email 來識別使用者。
驗證 ID Token###
收到 ID Token 後,必須驗證:
import { OAuth2Client } from 'google-auth-library';
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
async function verifyIdToken(idToken: string) {
const ticket = await client.verifyIdToken({
idToken,
audience: process.env.GOOGLE_CLIENT_ID,
});
const payload = ticket.getPayload();
// 驗證通過,payload 包含使用者資訊
return {
googleId: payload.sub,
email: payload.email,
name: payload.name,
picture: payload.picture,
};
}
驗證重點:
- 簽名:用 Google 的公鑰驗證 JWT 簽名
- iss:必須是
accounts.google.com或https://accounts.google.com - aud:必須是你的
client_id - exp:必須未過期
- nonce:必須與授權請求中的 nonce 一致
Security##
PKCE(Proof Key for Code Exchange)###
PKCE 是 Authorization Code Flow 的安全強化——OAuth 2.1 中已成為所有 client 的強制要求:
sequenceDiagram
participant C as Your App
participant A as Auth Server
Note over C: Generate code_verifier (random)
Note over C: Compute code_challenge = SHA256(code_verifier)
C->>A: /authorize + code_challenge
A->>C: Redirect with auth code
C->>A: /token + code + code_verifier
Note over A: Verify SHA256(code_verifier) == code_challenge
A->>C: Return tokens
import crypto from 'crypto';
// Step 1: Generate code_verifier (43-128 characters)
const codeVerifier = crypto.randomBytes(32).toString('base64url');
// Step 2: Compute code_challenge
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
// Step 3: Include in authorization request
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// Step 4: Include code_verifier in token exchange
const tokenParams = new URLSearchParams({
code: authorizationCode,
code_verifier: codeVerifier, // server verifies this matches
// ...other params
});
PKCE 防禦的攻擊:即使攻擊者截獲了 authorization code,沒有 code_verifier 就無法換取 token。
State Parameter###
state 參數防止 CSRF(Cross-Site Request Forgery)攻擊:
// 產生 state 並存入 session
const state = crypto.randomBytes(16).toString('hex');
session.oauthState = state;
// 回調時驗證
app.get('/callback', (req, res) => {
if (req.query.state !== session.oauthState) {
return res.status(403).send('CSRF detected');
}
// state 驗證通過,繼續處理
});
Token 儲存最佳實踐###
| Token | 儲存方式 | 原因 |
|---|---|---|
| Access Token | Server memory 或 httpOnly cookie | 避免 XSS 竊取 |
| Refresh Token | Server-side 資料庫(加密) | 長期有效,必須嚴格保護 |
| ID Token | 驗證後丟棄(只用 payload) | 只用來取得使用者資訊 |
// 設定 secure cookie
res.cookie('session', sessionId, {
httpOnly: true, // JavaScript 無法存取
secure: true, // 只透過 HTTPS 傳送
sameSite: 'lax', // 防止 CSRF
maxAge: 3600000, // 1 hour
});
Caution
永遠不要把 access token 或 refresh token 存在 localStorage 或非 httpOnly 的 cookie 中。這些位置容易被 XSS 攻擊竊取。
OAuth 2.0 安全 Checklist###
| 項目 | 說明 |
|---|---|
| 使用 PKCE | 所有 client 都應該啟用(OAuth 2.1 強制) |
| 驗證 state | 每次授權請求產生隨機 state,回調時驗證 |
| 驗證 nonce | ID Token 中的 nonce 必須與請求一致 |
| 白名單 redirect_uri | 只允許預先註冊的 URL |
| HTTPS only | 所有 OAuth 通訊必須走 HTTPS |
| 最小權限 scope | 只請求必要的權限 |
| Token 安全儲存 | httpOnly cookie 或 server-side |
| Refresh Token Rotation | 每次使用 refresh token 後發放新的 |
Quiz##
Summary##
- OAuth 2.0 是授權協定,OIDC 在其上加入認證——兩者搭配實現社群登入
- Authorization Code Flow 是最安全的流程:code 經 redirect 傳遞,token exchange 在 server-side 執行
- OIDC 的 ID Token(JWT)包含使用者身份,用
sub而非email識別使用者 - PKCE 防止 authorization code 被截獲冒用,OAuth 2.1 中已成為強制要求
state防 CSRF、nonce防 replay——兩者都是隨機字串,在回調時驗證- Token 安全儲存:
httpOnly+secure+sameSitecookie,不用 localStorage - Refresh Token 存在 server-side 資料庫,啟用 rotation(每次使用後換新)
留言 (0)
登入後即可留言