Skip to content
BEE
Backend Engineering Essentials

[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(*adminwrite:all)違反了最小權限原則,並在發生資安事件時擴大損害範圍。只請求當前操作所需的 scope。

P7 — 信任身分 claims 前,必須完整驗證 ID token。 ID token 是一個 JWT,所有 JWT 驗證規則均適用(見 BEE-1002):驗證簽名、issaudexpnonce(使用時)。不應以 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_challengecode_verifier 的 SHA-256 雜湊值;將此請求與令牌交換綁定
code_challenge_method=S256指定雜湊演算法;必須使用 S256plain 不安全)

第三步 — 授權伺服器攜帶授權碼重新導向回用戶端

使用者登入並核准後:

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 的被攔截授權碼無法被兌換。

回應:

json
{
  "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 之前,必須解碼並驗證:

json
{
  "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(簽名、expaud、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:orderswrite:orders),而非廣泛授權。

相關 BEE

參考資料