The Own Lab The Own Lab

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_typecode 表示使用 Authorization Code Flow
scope請求的權限(openid 啟用 OIDC)
stateCSRF 防護用的隨機字串
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.0OIDC
目的授權(Authorization)認證(Authentication)
回答的問題「這個 app 能存取什麼?」「這個使用者是誰?」
TokenAccess TokenAccess 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,
  };
}

驗證重點:

  1. 簽名:用 Google 的公鑰驗證 JWT 簽名
  2. iss:必須是 accounts.google.comhttps://accounts.google.com
  3. aud:必須是你的 client_id
  4. exp:必須未過期
  5. 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 TokenServer memory 或 httpOnly cookie避免 XSS 竊取
Refresh TokenServer-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,回調時驗證
驗證 nonceID Token 中的 nonce 必須與請求一致
白名單 redirect_uri只允許預先註冊的 URL
HTTPS only所有 OAuth 通訊必須走 HTTPS
最小權限 scope只請求必要的權限
Token 安全儲存httpOnly cookie 或 server-side
Refresh Token Rotation每次使用 refresh token 後發放新的

Quiz##

Single Choice

OAuth 2.0 和 OIDC 的關係是什麼?

Single Choice

在 Authorization Code Flow 中,為什麼要用 code 換 token,而不是直接回傳 token?

Single Choice

PKCE 防禦的是什麼攻擊?

Single Choice

為什麼應該用 ID Token 的 sub 而非 email 來識別使用者?

Single Choice

以下哪個 token 儲存方式最安全?

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 + sameSite cookie,不用 localStorage
  • Refresh Token 存在 server-side 資料庫,啟用 rotation(每次使用後換新)

留言 (0)

登入後即可留言