Ian Chou's Blog

深入理解 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! 🚀

2. 模擬「換取 Token」 (Exchange Token)

curl -X POST https://oauth.aicodecleanup.us/token
# Output: {"access_token":"mock_access_token_...","token_type":"Bearer", ...}

3. 模擬「存取受保護資料」 (Access Protected Resource)

curl -H "Authorization: Bearer test" https://oauth.aicodecleanup.us/userinfo
# Output: {"sub":"1234567890","name":"Mock User", ...}

Token 交換機制詳解

在真實世界的 OAuth 2.0 (Authorization Code Flow) 中,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 請求時,它會做以下幾件事:

  1. 檢查 client_idclient_secret 是否匹配
  2. 檢查 code 是否有效且尚未被使用(Code 只能用一次)
  3. 關鍵檢查:確認 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 安全性的核心:


Access Token 會變嗎?

會的,而且變得很頻繁。 這是一個重要的安全機制。

1. 生命週期極短 (Short-Lived)

Provider Access Token 有效期
Google 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..." 

結語

透過這篇文章,我們深入理解了:

下一步:如果你想讓 AI Agent 長期穩定運作,建議實作 「自動刷新 Token (Auto-Refresh)」 機制,這將在下一篇文章中詳細說明。


相關文章