[BEE-1003] OAuth 2.0 與 OpenID Connect
INFO
OAuth 2.0 以委派方式授予存取權限,無需共享憑證。OpenID Connect(OIDC)在其上新增了已驗證的身分層。單獨使用 OAuth 2.0 進行身分認證,是現代 API 安全中最常見、也最危險的誤解之一。
背景
在 OAuth 2.0 出現之前,應用程式若要存取第三方資源,必須直接向使用者索取憑證——這種模型違反了最小權限原則,且在不更改密碼的情況下無法撤銷存取權限。
RFC 6749(2012)將 OAuth 2.0 引入為授權委派框架(authorization delegation framework):資源擁有者(Resource Owner,使用者)授予用戶端(Client,應用程式)對資源伺服器(Resource Server)上資源的有限存取權,整個過程經由授權伺服器(Authorization Server)協調,用戶端無需直接接觸使用者的憑證。
OAuth 2.0 明確定位為授權(authorization)框架,規範中並未定義任何認證(authentication)機制。Access token 僅能證明某位資源擁有者授予了特定的 scope,並不能證明當前使用者的身分。這個區別被廣泛誤解:許多應用程式直接從 access token 推斷使用者身分,這在安全上是有問題的。
OpenID Connect(OIDC)Core 1.0 於 2014 年由 OpenID Foundation 發布,正是為了填補這個缺口。OIDC 是建立在 OAuth 2.0 之上的薄身分層(thin identity layer):它新增了一個 ID token(已簽名的 JWT),其中包含已驗證的終端使用者身分 claims,並標準化了 /userinfo 端點以提供額外的 claims。
RFC 7636(PKCE,2015)解決了對公開用戶端(public clients,如行動 App、單頁應用程式)的授權碼攔截攻擊問題。這類用戶端無法安全儲存 client secret。PKCE 透過密碼學挑戰,將授權請求與令牌交換綁定,使被攔截的授權碼對攻擊者毫無用處。
RFC 9700(OAuth 2.0 安全最佳現行實踐,2025)整合了累積的安全經驗,並明確廢棄了 Implicit grant 與 Resource Owner Password Credentials grant 在新部署中的使用。
原則
P1 — 使用 OAuth 2.0 進行委派授權,而非身分認證。 Access token 是能力令牌(capability token)——它證明了存取權已被授予,而非某位特定使用者正在線上。需要知道使用者身分時,應使用 OpenID Connect 並驗證 ID token。
P2 — 所有互動式流程均應使用 Authorization Code grant 搭配 PKCE。 這適用於 Web 應用程式、行動 App 和單頁應用程式。PKCE 對公開用戶端是必要的(RFC 9700 §2.1.1),且 RECOMMENDED 所有用戶端都採用。不應使用 Implicit grant。
P3 — 機器對機器(machine-to-machine)流程使用 Client Credentials。 當無使用者參與、服務以自身名義運作時,Client Credentials 是正確的 grant 類型。此流程中沒有使用者委派,也沒有 refresh token。
P4 — 新系統中永不使用 Implicit 或 Resource Owner Password Credentials grant。 Implicit grant 將 token 暴露在 URL fragment 中,繞過了 PKCE,且無令牌更新機制。Resource Owner Password Credentials grant 要求用戶端直接處理使用者憑證,完全破壞了委派模型。兩者均已被 RFC 9700 廢棄。
P5 — 在 callback 時必須驗證 state 參數。 state 參數是 CSRF 防禦措施。用戶端 MUST 生成隨機的 state 值,將其綁定到使用者 session,並在繼續處理之前驗證 callback 中攜帶的值是否一致。跳過此檢查將使攻擊者得以發起授權流程,並將其授權碼注入受害者的 session。
P6 — 只請求最低必要的 scope。 Scope 定義了委派存取的邊界。當較窄的 scope 已足夠時,請求廣泛的 scope(*、admin、write:all)違反了最小權限原則,並在發生資安事件時擴大損害範圍。只請求當前操作所需的 scope。
P7 — 信任身分 claims 前,必須完整驗證 ID token。 ID token 是一個 JWT,所有 JWT 驗證規則均適用(見 BEE-1002):驗證簽名、iss、aud、exp 及 nonce(使用時)。不應以 access token 或 /userinfo 回應作為 ID token 驗證的替代方案。
圖解
以下流程圖展示了 Authorization Code flow 搭配 PKCE 的完整過程,涵蓋所有四個角色及重新導向、授權碼交換與 API 存取的完整序列。
範例
以下使用通用 HTTP 請求展示完整的 Authorization Code + PKCE 流程。所有值均為示意用途。
第一步 — 生成 PKCE 參數
在重新導向使用者之前,用戶端生成隨機密鑰並推導挑戰值:
code_verifier = base64url(random_bytes(32))
= "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
code_challenge = base64url(SHA256(code_verifier))
= "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"第二步 — 授權請求(用戶端將使用者重新導向)
GET /authorize
?response_type=code
&client_id=app-client-123
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=openid%20profile%20email%20read%3Adocuments
&state=af0ifjsldkj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
Host: auth.example.com關鍵參數說明:
| 參數 | 用途 |
|---|---|
response_type=code | 請求授權碼(而非直接取得 token) |
scope=openid ... | openid scope 啟動 OIDC;其他 scope 定義 API 存取範圍 |
state | 綁定至 session 的隨機值;MUST 在 callback 時驗證 |
code_challenge | code_verifier 的 SHA-256 雜湊值;將此請求與令牌交換綁定 |
code_challenge_method=S256 | 指定雜湊演算法;必須使用 S256(plain 不安全) |
第三步 — 授權伺服器攜帶授權碼重新導向回用戶端
使用者登入並核准後:
HTTP/1.1 302 Found
Location: https://app.example.com/callback
?code=SplxlOBeZQQYbYS6WxSbIA
&state=af0ifjsldkj用戶端 MUST 在繼續處理之前,驗證 state 與儲存在使用者 session 中的值一致。
第四步 — 令牌交換
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&client_id=app-client-123
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk授權伺服器驗證 SHA256(code_verifier) == code_challenge(第二步中的值)。沒有 code_verifier 的被攔截授權碼無法被兌換。
回應:
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "8xLOxBtZp8",
"id_token": "eyJhbGciOiJSUzI1NiJ9...",
"scope": "openid profile email read:documents"
}第五步 — 驗證 ID token
ID token 是已簽名的 JWT。在信任任何 claims 之前,必須解碼並驗證:
{
"iss": "https://auth.example.com",
"sub": "user-7f3a9b",
"aud": "app-client-123",
"exp": 1712530800,
"iat": 1712530200,
"nonce": "n-0S6_WzA2Mj",
"email": "alice@example.com",
"name": "Alice"
}驗證 MUST 包含:簽名(以授權伺服器公鑰驗證)、iss 與已知簽發者一致、aud 與你的 client_id 一致、exp 尚未過期、nonce 與第二步發送的值一致(使用 nonce 時)。
第六步 — 使用 access token 呼叫 API
GET /api/documents HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...資源伺服器驗證 access token(簽名、exp、aud、scope)後回傳資源。Access token 證明了對 read:documents 的委派存取——它本身並不獨立向資源伺服器證明呼叫方的身分(這是 ID token 流程的職責)。
令牌比較
| 令牌 | 用途 | 由誰驗證 | 典型有效期 |
|---|---|---|---|
| Access token | 授權 API 呼叫(委派存取) | 資源伺服器 | 15 分鐘 |
| ID token | 認證終端使用者(僅 OIDC) | 用戶端應用程式 | 單次使用(驗證一次即可) |
| Refresh token | 取得新的 access token | 授權伺服器 | 數小時至數天 |
常見錯誤
1. 單獨使用 OAuth 2.0 進行身分認證。
使用 access token 呼叫 GET /userinfo 並不能向你的應用程式認證使用者——它只是從授權伺服器取得 claims,但沒有與當前 session 或使用者互動行為的綁定關係。應使用 OpenID Connect:驗證 ID token 的簽名與 nonce claim,以確認特定使用者確實在此特定 session 中完成了認證。(AuthN/AuthZ 的區別詳見 BEE-1001。)
2. 在新應用程式中使用 Implicit grant。
Implicit grant 直接在 URL fragment(#access_token=...)中回傳 access token。URL fragment 中的 token 會被 proxy 記錄、儲存於瀏覽器歷史,且對頁面上的 JavaScript 可見。Implicit grant 是在 PKCE 出現之前設計的,已被 RFC 9700 廢棄。應改用 Authorization Code + PKCE——無需 client secret 即可適用於 SPA 和行動 App。
3. 未驗證 state 參數。
忽略 callback 中 state 值的伺服器容易遭受 CSRF 攻擊。攻擊者可以用自己的授權碼發起 OAuth 流程,誘騙受害者的瀏覽器完成整個流程,從而將攻擊者的身分(或存取權限)綁定到受害者的 session。每次請求應生成加密隨機的 state,將其綁定到 session,並拒絕任何 state 不一致的 callback。
4. 請求過於廣泛的 scope。
scope=* 或 scope=admin 會請求授權伺服器支援的所有權限。當用戶端日後遭到攻擊時,每個 scope 都將暴露給攻擊者。過寬的 scope 也會侵蝕使用者對同意授權畫面的信任。應只請求當前操作所需的最小 scope 集合。對於透過 Client Credentials 進行的 API 對 API 存取,應定義細粒度的資源 scope(read:orders、write:orders),而非廣泛授權。
相關 BEE
- BEE-1001: Authentication vs Authorization — OAuth 2.0 與 OIDC 所跨越的身分認證與授權概念邊界
- BEE-1002: Token-Based Authentication — JWT 結構、驗證與令牌生命週期
- BEE-2004: Third-Party API Integration Security — OAuth 在外部服務整合情境下的應用
參考資料
- Hardt, D. (ed.), "The OAuth 2.0 Authorization Framework" RFC 6749 (2012). https://datatracker.ietf.org/doc/html/rfc6749
- Sakimura, N. et al., "Proof Key for Code Exchange by OAuth Public Clients" RFC 7636 (2015). https://datatracker.ietf.org/doc/html/rfc7636
- Sakimura, N. et al., "OpenID Connect Core 1.0 incorporating errata set 2" (2023). https://openid.net/specs/openid-connect-core-1_0.html
- Lodderstedt, T. et al., "Best Current Practice for OAuth 2.0 Security" RFC 9700 (2025). https://datatracker.ietf.org/doc/rfc9700/
- Parecki, A., "OAuth 2.0 Simplified" (2020). https://oauth2simplified.com/
- Jones, M. and Hardt, D., "The OAuth 2.0 Authorization Framework: Bearer Token Usage" RFC 6750 (2012). https://datatracker.ietf.org/doc/html/rfc6750
- OWASP, "OAuth 2.0 Cheat Sheet" (2024). https://cheatsheetseries.owasp.org/cheatsheets/OAuth2_Cheat_Sheet.html