深入理解 OAuth 2.0:從 Client ID 到 Token Exchange
深入理解 OAuth 2.0:從 Client ID 到 Token Exchange
本文以 CISSP 資安視角與全端開發實務,拆解 OAuth 2.0 中最關鍵的概念:身分識別、Token 交換與資料存取。
OAUTH_CLIENT_ID 是什麼?
簡單來說,OAUTH_CLIENT_ID 是你的應用程式(Application)在 OAuth 服務供應商(如 Google, GitHub, Facebook)那裡的 「身分證字號」。
在 OAuth 2.0 協議中,這代表的是 「應用程式的公開識別碼」 (Public Identifier)。
概念層面:它是「App 的帳號」
當你在開發一個 Next.js 網站,並希望使用者能用 Google 登入時,Google 需要知道是「誰」在發出請求:
| 角色 | 說明 |
|---|---|
| User (Resource Owner) | 真人使用者,有 Google 帳號 |
| Application (Client) | 你的程式,它也需要在 Google 那裡有一個「帳號」 |
OAUTH_CLIENT_ID 就是這個應用程式的帳號名稱。當使用者看到授權畫面寫著「MyAwesomeApp 想要存取您的 Email」時,Google 就是透過 Client ID 查出這個 App 的名稱並顯示出來的。
技術實作層面:它是 URL 參數
當你的 Next.js App 引導使用者去登入時,你會由前端發起一個 Redirect。這個 URL 會長這樣:
https://github.com/login/oauth/authorize?
client_id=YOUR_CLIENT_ID_12345& ← 告訴 GitHub 是哪個 App 來了
redirect_uri=http://localhost:3000/callback&
scope=user:email&
state=xyz
因為它是透過瀏覽器網址列傳遞的,所以 CLIENT_ID 被視為公開資訊 (Public Information),它可以暴露在前端程式碼中(不像 CLIENT_SECRET 絕對不能暴露)。
資安層面 (CISSP 觀點):識別 vs 驗證
在 IAM (Identity and Access Management) 的概念中:
| 概念 | 元件 | 說明 |
|---|---|---|
| 識別 (Identification) | OAUTH_CLIENT_ID |
告訴 Server「我是誰」。就像員工識別證號碼,別人知道也沒關係 |
| 驗證 (Authentication) | OAUTH_CLIENT_SECRET |
證明「我真的是這個 ID 的擁有者」。絕對只能放在後端 |
驗證 Mock Server 的三個階段
當你成功部署 Mock OAuth Server 後,可以透過 curl 指令手動驗證 OAuth 2.0 的三個關鍵階段:
1. 確認服務存活 (Health Check)
curl https://oauth.aicodecleanup.us/
# Output: OAuth 2.0 Mock Server is Running on Cloudflare! 🚀
- 意義:Cloudflare Worker 已上線,可被公開存取
- 用途:就像 Ping 一樣,確認伺服器沒掛掉
2. 模擬「換取 Token」 (Exchange Token)
curl -X POST https://oauth.aicodecleanup.us/token
# Output: {"access_token":"mock_access_token_...","token_type":"Bearer", ...}
- 意義:模擬 OAuth 流程中的 「後端對後端 (Back-channel)」 溝通
- 發生了什麼:
- 在真實世界,你的 Server 會拿著
Authorization Code和Client Secret跟 Google 說:「這是 code,請給我 token」 - Mock Server 不檢查,直接發給你一張 Access Token
- 在真實世界,你的 Server 會拿著
3. 模擬「存取受保護資料」 (Access Protected Resource)
curl -H "Authorization: Bearer test" https://oauth.aicodecleanup.us/userinfo
# Output: {"sub":"1234567890","name":"Mock User", ...}
- 意義:模擬 「帶著 Token 去拿資料」 的動作
- 應用場景:就像你的 MCP Server 拿著 Token 去問 Google:「這個使用者的 Email 是什麼?」
Token 交換機制詳解
在真實世界的 OAuth 2.0 (Authorization Code Flow) 中,Token 交換是整個流程中 最關鍵、也是最敏感 的一步。
場景設定
- 你的手上 (MCP Server):剛從前端 Redirect 網址列抓到的
code - 你的身分:Google 認證過的 App,持有
client_id和client_secret - 你的目標:拿到
access_token
HTTP 請求結構
你的 MCP Server 必須發出一個 POST 請求。
目標 URL (以 Google 為例):
https://oauth2.googleapis.com/token
Headers:
Content-Type: application/x-www-form-urlencoded (規範要求,不能用 JSON)
Accept: application/json
Body 參數 (缺一不可):
| 參數 | 範例值 | 意義 (CISSP 視角) |
|---|---|---|
code |
4/0Afge... |
一次性票據。使用者同意後的臨時憑證,有效期約 10 分鐘 |
client_id |
883...apps.googleusercontent.com |
身分識別。告訴 Google 是哪個 App 在換 Token |
client_secret |
GOCSPX-d1... |
身分驗證。私鑰,絕對不能暴露在瀏覽器 |
redirect_uri |
http://localhost:3000/callback |
防護機制。必須與第一步完全一致,防止 Code 被駭客攔截 |
grant_type |
authorization_code |
意圖宣告。表示「我是拿 Code 來換 Token 的」 |
Google 內部的驗證邏輯
當 Google 收到這個 POST 請求時,它會做以下幾件事:
- 檢查
client_id與client_secret是否匹配 - 檢查
code是否有效且尚未被使用(Code 只能用一次) - 關鍵檢查:確認
redirect_uri是否與當初生成code時填寫的完全一致。不一樣就拒絕
伺服器回傳
驗證通過後,Google 回傳 HTTP 200 和一個 JSON 物件:
{
"access_token": "ya29.a0Af...",
"expires_in": 3599,
"scope": "https://www.googleapis.com/auth/calendar",
"token_type": "Bearer",
"id_token": "eyJhbGciOiJSU...",
"refresh_token": "1//04..."
}
| 欄位 | 說明 |
|---|---|
access_token |
你要的鑰匙 (Bearer Token) |
expires_in |
有效期(秒),通常 1 小時 |
refresh_token |
用來在過期後自動續期 Token |
id_token |
(OIDC) 包含使用者資訊的 JWT |
程式碼範例 (Node.js / Bun)
const tokenEndpoint = "https://oauth2.googleapis.com/token";
const response = await fetch(tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
code: "剛剛拿到的_code",
client_id: process.env.GOOGLE_CLIENT_ID!,
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
redirect_uri: "http://localhost:3000/callback",
grant_type: "authorization_code",
}),
});
const data = await response.json();
if (!response.ok) {
console.error("Token Exchange Failed:", data);
throw new Error("Failed to get token");
}
console.log("Got Access Token:", data.access_token);
// 接下來:把 token 存入 DB 或 Session
為什麼這步這麼重要?
這一步是 OAuth 2.0 安全性的核心:
- 如果是前端直接拿 Token (Implicit Flow),Token 會暴露在 URL 或瀏覽器 History 中
- 透過 Code Exchange 流程,真正的鑰匙 (
access_token) 和永久續期票 (refresh_token) 是直接從 Google 傳送到你的後端 Server - 全程加密且不經過使用者的瀏覽器,大幅降低被 XSS 或網路竊聽攻擊竊取的風險
Access Token 會變嗎?
會的,而且變得很頻繁。 這是一個重要的安全機制。
1. 生命週期極短 (Short-Lived)
| Provider | Access Token 有效期 |
|---|---|
| 1 小時 | |
| GitHub | 8 ~ 24 小時不等 |
| 銀行/金融 API | 5 ~ 15 分鐘 |
一旦時間到了,Token 就會作廢,必須申請新的。
2. JWT 簽名機制
如果 Token 是 JWT (JSON Web Token) 格式,每次申請新 Token 時,以下欄位一定會變:
| 欄位 | 說明 |
|---|---|
iat (Issued At) |
核發時間 |
exp (Expiration Time) |
過期時間 |
jti (JWT ID) |
該次 Token 的唯一流水號 |
根據雜湊演算法原理,只要原始資料變了一個字,最後生成的簽名就會完全不同。
3. Refresh Token Rotation
真實運作流程:
09:00 取得 Token A (有效 1 小時)
使用 Authorization: Bearer Token_A
09:30 使用 Token A (成功)
10:01 使用 Token A → Server 回傳 401 Unauthorized (Token expired)
10:01 程式偵測到 401,拿 refresh_token 去換新的
10:02 取得 Token B (有效 1 小時)
從現在開始,改用 Authorization: Bearer Token_B
Mock Server 的狀況
回頭看 Mock Server 程式碼:
access_token: `mock_access_token_${Date.now()}`,
因為用了 Date.now(),連續呼叫 /token 兩次會得到兩組不同的字串。
但 /userinfo 驗證邏輯只檢查「有 Bearer 就好」:
if (authHeader && authHeader.startsWith('Bearer '))
所以在 Mock 環境中,用舊的、新的、甚至亂打的 Bearer abcdefg 都能過關。這在測試連線時非常方便。
整合到 MCP Client
現在你可以把設定檔填上真實網址了:
const config = {
oauth: {
authorizationUrl: "https://oauth.aicodecleanup.us/authorize",
tokenUrl: "https://oauth.aicodecleanup.us/token",
userInfoUrl: "https://oauth.aicodecleanup.us/userinfo",
clientId: "any_string", // Mock Server 不在意
clientSecret: "any_string", // Mock Server 不在意
}
};
.env 檔案範例
# 公開的 ID,告訴 Provider 是哪個 App
OAUTH_CLIENT_ID="iv1.8a6549c8f..."
# 私密的密碼,用來交換 Access Token
OAUTH_CLIENT_SECRET="9b7d..."
結語
透過這篇文章,我們深入理解了:
- ✅
OAUTH_CLIENT_ID的本質與安全性 - ✅ Token 交換機制的完整流程
- ✅ Access Token 為何會變,以及安全考量
- ✅ 如何整合 Mock OAuth Server 到 MCP Client
下一步:如果你想讓 AI Agent 長期穩定運作,建議實作 「自動刷新 Token (Auto-Refresh)」 機制,這將在下一篇文章中詳細說明。
相關文章
- ← Previous
OAuth 2.0 測試實戰(Mock Provider + Inspector):用 Bearer Token 保護 MCP SSE - Next →
WSL 2 圖形界面完整指南:Wayland、WSLg 與 Windows 互通