題目與適用情境
你的產品需要代表使用者讀取第三方行事曆,但不能接觸使用者在行事曆服務的密碼。請解釋公開用戶端使用 OAuth 2.0 授權碼流程與 PKCE 的完整過程,並說明後端與授權伺服器各自必須驗證什麼。
本題假設用戶端是瀏覽器單頁應用程式或原生應用程式,無法可靠保存所有安裝實例共用的靜態密鑰,因此屬於 公開用戶端。授權伺服器負責驗證使用者並取得授權,資源伺服器提供行事曆 API,用戶端取得存取權杖後呼叫 資源伺服器。授權範圍先限定為唯讀行事曆;是否簽發更新權杖由授權伺服器政策決定。
2026 年 3 月發布的公開後端面試指南直接要求候選人解釋授權碼模式、PKCE、公開與機密用戶端,以及 OAuth 與 OpenID Connect 的差異。目前安全基線還要以 OAuth 2.0 Security Best Current Practice 為準: 公開用戶端必須使用 PKCE,機密用戶端也建議使用,授權伺服器必須防止 PKCE 降級,並對已註冊回呼位址執行 精確比對。
這是一題 backend 類別,因為核心能力是設計與驗證跨服務授權協定、權杖邊界及安全控制。前端跳轉只是協定載體。
面試官考察重點
第一,候選人是否先分清四個角色:資源擁有者是使用者,用戶端是行事曆整合產品,授權伺服器負責登入、同意與 發放權杖,資源伺服器持有行事曆資料。把「用戶端」和「使用者」混在一起,後續的權限與權杖接收者都會說錯。
第二,能否解釋授權碼為什麼分成前後兩段。瀏覽器只帶回短期、一次性的授權碼;存取權杖由用戶端在權杖端點 交換取得,不放入瀏覽器跳轉 URL。PKCE 再把授權請求產生的隨機秘密與這次程式碼交換綁定,截取授權碼的一方 沒有 code_verifier,仍然不能換取權杖。
第三,能否準確區分控制的職責:
| 控制 | 主要綁定關係 | 主要處理的問題 | |---|---|---| | state | 發起請求的用戶端工作階段與回呼 | 請求關聯、登入 CSRF | | PKCE | 授權請求與權杖交換 | 授權碼截取、程式碼注入 | | 用戶端驗證 | 機密用戶端與授權伺服器 | 證明執行權杖交換的用戶端身分 | | 精確回呼位址 | 已註冊用戶端與允許的回呼端點 | 授權碼被送到攻擊者控制的位址 | | OIDC nonce | 登入請求與 ID Token | ID Token 重播與登入工作階段關聯 |
最後,好的回答會主動說明 PKCE 的邊界。它無法防止同一頁面中的 XSS 讀取 verifier 或權杖,不會取代 TLS、 回呼位址驗證、最小權限與權杖安全保存,也不會讓 OAuth 自動變成登入協定。
回答前需要釐清的問題
- 用戶端是公開還是機密用戶端? SPA 與原生應用程式不能靠靜態 client secret 證明身分;有受控後端的
Web 應用程式可以成為機密用戶端,在 PKCE 之外還要執行用戶端驗證。
- 目標是呼叫 API,還是讓使用者登入產品? 呼叫行事曆 API 使用 OAuth;需要使用者身分時應使用 OIDC,
並驗證 ID Token,不能從 access token 猜測使用者是誰。
- 用戶端只連接一個授權伺服器,還是允許租戶自帶身分提供者? 多授權伺服器情境還要驗證 issuer,或為
每個 issuer 使用不同回呼位址,防止 mix-up 攻擊。
- 需要離線存取嗎? 不需要時不要申請更新權杖;需要時必須定義更新權杖輪替、重播偵測、撤銷與失效政策。
- 回呼交易保存在哪裡? 必須保存一組
state → code_verifier綁定。單一全域 verifier 會讓並行分頁
互相覆蓋;把 verifier 放入 URL 或紀錄又會破壞它的機密性。
- 行事曆權限需要多大? 預設只申請目前功能需要的唯讀 scope;寫入與整個帳號權限要由具體功能證明必要性。
30 秒回答框架
「我會讓用戶端為每次授權產生不可預測的 state 與高熵 code_verifier,用 S256 得到 codechallenge。瀏覽器重新導向授權端點,帶上 responsetype=code、client ID、精確註冊的回呼位址、 最小 scope、state 與 challenge。使用者只在授權伺服器登入並同意,回呼只傳回短期一次性 code 與原樣 state。 用戶端先驗證 state 並取回這次交易的 verifier,再向權杖端點提交 code、回呼位址和 verifier。授權伺服器驗證 code、client、redirect URI 與 challenge 綁定後才發存取權杖。公開用戶端不能把靜態 secret 當憑證;機密 用戶端還要做用戶端驗證。PKCE 防止授權碼被截取或注入,state 負責請求關聯,OIDC 的 ID Token 才用於登入身分。」
分步深入解答
第一步:先建立一次性的授權交易。
用戶端為每次點擊「連接行事曆」建立兩份互相獨立的隨機值:
state:不可猜測的交易 ID,綁定目前使用者工作階段、預期授權伺服器、回呼位址與授權後的站內去向。code_verifier:使用密碼學安全亂數產生。RFC 7636 要求它由未保留字元組成,長度為 43 到 128 個字元,
並建議至少提供 256 位元熵。
用戶端使用下列轉換產生 challenge:
code_challenge = base64url_without_padding(
SHA256(ASCII(code_verifier))
)
code_challenge_method = S256每個授權交易分別保存 state → code_verifier。瀏覽器可能同時有兩個行事曆連接流程,因此不能只保存 「最後一個 verifier」。交易還要有短期到期時間,並在成功或失敗後刪除。verifier 不應進入跳轉 URL、分析事件 或應用程式紀錄。
第二步:建立授權請求。
用戶端透過使用者代理程式存取授權端點:
GET /authorize
?response_type=code
&client_id=calendar-client
&redirect_uri=registered-callback
&scope=calendar.read
&state=random-transaction-id
&code_challenge=derived-challenge
&code_challenge_method=S256授權伺服器驗證 client ID、回應類型、scope 與回呼位址。回呼位址應與預先註冊值精確相符,不能使用寬泛網域、 任意子路徑或使用者可控制的跳轉參數。使用者憑證只提交給授權伺服器;用戶端接觸的是授權結果,不是使用者密碼。
第三步:處理回呼,但先不要呼叫權杖端點。
使用者同意後,授權伺服器把使用者代理程式重新導向已註冊回呼,並帶回 code 與原樣 state。用戶端先執行 本地交易驗證:
state是否存在、尚未過期且未被消耗。- 它是否綁定目前瀏覽器工作階段與預期授權伺服器。
- 目前回呼是否到達這次交易登記的回呼端點。
- 如果回應是錯誤,是否仍能依 state 找到並安全結束對應交易。
state 不相符時直接終止,不能「先換權杖看看是否成功」。授權碼應短期且只能使用一次;授權伺服器發現重複交換 必須拒絕,並在可能時撤銷根據該 code 已簽發的權杖。
第四步:用 verifier 交換權杖。
用戶端向權杖端點傳送:
POST /token
grant_type=authorization_code
code=returned-authorization-code
redirect_uri=registered-callback
client_id=calendar-client
code_verifier=stored-verifier授權伺服器需要把目前請求與最初授權交易重新組合:code 屬於哪個 client、使用哪個 redirect URI、是否過期或 使用過、授權請求是否包含 challenge,以及目前 verifier 經 S256 計算後是否等於已保存的 challenge。如果原始 請求沒有 challenge,權杖請求卻收到 verifier,伺服器不能悄悄降級為無 PKCE 流程。
公開用戶端傳送 client ID 用於識別,但不能靠公開程式碼中的靜態 secret 證明身分。機密用戶端在相同 code 與 PKCE 驗證之外,還要使用約定的用戶端驗證方式;PKCE 不會取代用戶端驗證。
第五步:用攻擊路徑解釋每個控制為什麼存在。
假設合法用戶端產生 verifier V1 與 challenge C1。攻擊者截取回呼中的 code,卻不知道 V1。攻擊者向 權杖端點提交自己的 verifier,計算結果不等於 C1,交換失敗。這是 PKCE 對授權碼截取的核心保護。
再假設攻擊者發起自己的授權交易,接著嘗試把自己的 code 注入受害者回呼。受害者交易保存的是另一組 state 與 verifier;state 關聯或 PKCE 綁定至少有一處不相符,用戶端必須終止。授權伺服器同時要記錄「該 code 是否帶 challenge」並強制驗證,避免攻擊者刪除 challenge 後觸發降級。
如果攻擊者把回呼位址改成自己控制的端點,精確註冊與精確字串比對應在授權請求階段拒絕。若用戶端支援多個 授權伺服器,還必須比較回應的 issuer 與交易中保存的 issuer,或使用每個 issuer 獨立的回呼位址;只記住一個 授權端點 URL 不足以防止 mix-up。
第六步:把權杖限制在真實用途內。
存取權杖代表授權,不應由用戶端當作使用者身分證明。資源伺服器要驗證權杖是否面向自己,並限制到所需資源與 動作。用戶端只申請 calendar.read,存取權杖設定短效期,並避免進入 URL、紀錄和無關指令碼可讀取的持久儲存。
PKCE 在 code 換成 token 後已完成職責。XSS 如果能讀取 SPA 記憶體、交易儲存或存取權杖,PKCE 無法挽回。 高風險瀏覽器應用程式可採用 BFF:權杖保存在受控後端,瀏覽器只持有 HttpOnly 工作階段 cookie。這個選擇減少 JavaScript 直接接觸權杖的機會,但會帶來 cookie 工作階段、CSRF 防護、後端擴充與代理 API 的成本。
若公開用戶端取得更新權杖,授權伺服器必須使用傳送者約束或更新權杖輪替來偵測重播。輪替時每次更新都發新 refresh token 並讓舊值失效;舊值再次出現代表可能外洩,應撤銷這條授權鏈並要求重新授權。
第七步:分清 OAuth、OIDC 與其他授權方式。
OAuth 回答「用戶端可否代表使用者存取某項資源」。OIDC 在 OAuth 上增加身分層,用戶端透過 ID Token 驗證 使用者登入。OIDC 回呼還要驗證簽章、issuer、audience、有效期與 nonce;access token 面向資源伺服器, ID Token 面向用戶端,兩者不能互換。
沒有使用者參與、服務代表自身呼叫受控資源時,才考慮 client credentials。裝置輸入受限時可使用裝置授權流程。 隱式流程把存取權杖放進授權回應,增加 URL 外洩與重播面;目前最佳實務建議使用回傳 code 的流程。資源擁有者 密碼憑證模式會讓用戶端直接接觸使用者密碼,目前安全基線明確禁止使用。
第八步:用失敗測試驗收協定,不只跑通 happy path。
| 測試 | 預期結果 | |---|---| | 修改一個 verifier 字元 | 權杖交換失敗 | | 同一個 code 交換兩次 | 第二次失敗,並觸發相應安全處理 | | 回呼 state 不存在、過期或屬於另一工作階段 | 用戶端在呼叫權杖端點前拒絕 | | 授權請求缺少 challenge,權杖請求卻帶 verifier | 授權伺服器拒絕降級 | | redirect URI 只有大小寫、路徑或結尾斜線不同 | 精確比對失敗 | | 兩個分頁同時發起授權並以不同順序回傳 | 各自依 state 找到正確 verifier | | 連接兩個授權伺服器後交換 issuer | 用戶端拒絕 mix-up | | 掃描紀錄與分析事件 | 不出現 code、verifier、access token 或 refresh token | | XSS 讀取瀏覽器可存取儲存 | 明確認定 PKCE 不提供此項保護,並評估 BFF |
正式環境監控還要區分授權拒絕、state 驗證失敗、PKCE 驗證失敗、code 重複使用、redirect 不相符與更新權杖 重播。只統計「OAuth 失敗」會掩蓋攻擊訊號與用戶端實作錯誤。
高品質示範回答
「我先把情境定為公開用戶端存取第三方行事曆 API。用戶端無法安全保存共用 secret,所以我不會把寫在 SPA 套件裡的 secret 當作用戶端驗證。
每次授權開始時,我產生不可預測的 state 與至少 256 位元熵的 code verifier,使用 S256 得到 challenge, 並依 state 保存這次交易的 verifier、工作階段、issuer、回呼位址與到期時間。授權請求帶上 code 回應類型、 client ID、精確註冊的回呼位址、唯讀 scope、state 和 challenge。使用者只在授權伺服器登入與同意。
回呼收到 code 和 state 後,我先驗證 state 屬於目前工作階段且尚未消耗,再取回對應 verifier。接著向 token endpoint 傳送 code、同一 redirect URI、client ID 與 verifier。授權伺服器確認 code 短期未使用、屬於該 用戶端和回呼位址,並驗證 verifier 的 S256 結果與 challenge 相等,才簽發存取權杖。攻擊者即使截到 code, 沒有 verifier 也換不到權杖;如果刪除 challenge 嘗試降級,授權伺服器也必須拒絕。
state 負責請求關聯,PKCE 綁定 code 交換,不能用一個含糊的『防 CSRF 參數』概括。機密 Web 應用程式還要做 用戶端驗證;PKCE 仍建議保留。存取權杖只用於行事曆資源伺服器,若產品要把這個流程當登入,還應使用 OIDC, 並驗證 ID Token 的簽章、issuer、audience、有效期與 nonce。
最後我會測試錯誤 verifier、重複 code、state 跨工作階段、精確回呼不相符、並行分頁、PKCE 降級和多 issuer 混淆,並確認紀錄裡沒有 code 或權杖。PKCE 無法防止同源 XSS 偷走瀏覽器中的 verifier 或 token;風險高時 我會評估由 BFF 保存權杖,用 HttpOnly 工作階段 cookie 連接瀏覽器。」
常見錯誤
- 把 client ID 當成 secret → client ID 會出現在授權請求中,只用來識別用戶端 → **公開用戶端使用 PKCE,
機密用戶端另外執行真正的用戶端驗證。**
- 只說「code 比 token 安全」 → 沒有說明截取 code 後為何不能交換 → **畫出 challenge 與 verifier 綁定,
並說明權杖端點的比較。**
- 使用
plain或允許缺少 PKCE → verifier 可能在授權請求中外洩,或攻擊者可觸發降級 → **使用 S256,
並讓授權伺服器記錄與強制執行 PKCE。**
- 產生 state 卻不綁定工作階段 → 攻擊者可以搬運合法 state,或並行流程互相覆蓋 → **保存一次性的 state、
工作階段、issuer 與 verifier 對應。**
- 允許模糊回呼比對 → 授權碼可能被送到攻擊者控制的子路徑或跳轉器 → 預先註冊並精確比對 redirect URI。
- 把 access token 解碼後直接當登入身分 → token 的接收者與語意可能是資源伺服器 → **登入使用 OIDC,
依規範驗證 ID Token。**
- 宣稱 PKCE 可以防 XSS → XSS 可能直接讀取 verifier、code 或已簽發權杖 → **減少瀏覽器權杖暴露,
修正 XSS,並依風險評估 BFF。**
- 把 code、verifier 或 token 寫入紀錄 → 診斷系統擴大秘密的可見範圍 → **記錄交易 ID、錯誤分類與雜湊
關聯值,不記錄可兌換憑證。**
追問及應對
追問 1:如果用戶端改成有後端的傳統 Web 應用程式,還需要 PKCE 嗎?
需要。機密用戶端在權杖端點執行用戶端驗證,證明交換者持有受保護憑證;PKCE 綁定的是本次授權請求與 code 交換,可進一步防止授權碼注入和誤用。兩者保護不同關係。verifier 與 state 可保存在伺服器端工作階段中, 瀏覽器只攜帶不可預測的工作階段識別碼。
追問 2:如果這個流程還承擔「使用行事曆帳號登入」,要增加什麼?
使用 OIDC,不要把 access token 或 UserInfo 回應隨意當成登入證明。請求包含 openid scope 與 nonce, 用戶端驗證 ID Token 簽章、issuer、audience、有效期及 nonce,並以 issuer 與 subject 組合成外部身分鍵。 state 繼續關聯瀏覽器交易,nonce 關聯登入請求與 ID Token。
追問 3:如果 SaaS 允許每個租戶設定不同授權伺服器,最大的新風險是什麼?
用戶端可能把來自授權伺服器 A 的回應送到授權伺服器 B 的 token endpoint,形成 mix-up。交易中要保存預期 issuer,並驗證回應 issuer;另一種作法是為每個 issuer 設定獨立 redirect URI,並驗證回應到達正確端點。 授權端點與權杖端點必須來自同一份可信 issuer 中繼資料。
追問 4:公開用戶端必須長期在背景同步行事曆,更新權杖如何保護?
只授予必要 scope,並使用更新權杖輪替或傳送者約束。輪替時保存 token family:每次更新廢棄舊值,舊 refresh token 再出現就撤銷目前家族並要求重新授權。用戶端還需要安全儲存、撤銷入口、最長壽命及長時間未使用後的 到期政策。PKCE 只保護最初 code 交換,不保護之後被竊取的 refresh token。
追問 5:兩個分頁同時連接不同行事曆帳號,為什麼會偶發失敗?
常見原因是用戶端只保存一個全域 state 或 verifier,第二個分頁覆蓋第一個。回呼必須依 state 查找獨立交易, 交易中保存 verifier、預期帳號或 issuer、回呼位址與建立時間。消耗後原子刪除;未知、重複或過期 state 全部拒絕,不能退回「最近一次 verifier」。