目錄

Proof Key for Code Exchange

OAuth2 定義兩種 Client 類型:confidentialpublic 兩種。之前不管 OAuth2 或是 OpenID Connect,在說明流程的時候,都是以 confidential client,搭配較安全的 Authorization Code Grant / Authorization Code Flow 作為範例,但終究還是會面對相較不安全的 public client。

RFC 7636 裡有提到 public client 容易受到攻擊的方法之一--authorization code interception attack,中文直譯為「授權碼攔截攻擊」,顧名思義,它的攻擊手法基礎正是把 authorization code 偷走。

之前討論到 OAuth2 的 Authorization Code GrantOpenID Connect 時,有提到 code 用途主要是拿來交換 token;而在簡介 HTTP 協定有提到,HTTP 的特色即是無狀態,也就是只要請求內容一模一樣,則它無法分辨請求是否是惡意程式所發出的,因此只要攻擊者能偽造請求,即可成功使用 code 換取 token。

那回頭看一下 Client 會傳什麼資訊給 token endpoint:

  • grant_type
  • code
  • redirect_uri

除了上面三個參數外,另外還需要傳 Client ID 與 secret 給授權服務器驗證,即可換到 token。理論上憑證資訊與 token 都是不能外流的,但 public client 的特色即是 Client 處於相較不安全的環境,如 Native App 只要匯出後解開,即可找到憑證資訊。

以 Native App 為例,攻擊手法的時序圖如下:

@startuml
participant LegitimateApp
participant MaliciousApp
participant Browser
participant AuthorizationServer
LegitimateApp -> Browser: (1) Authorization Request
Browser -> AuthorizationServer: (2) Authorization Request
Browser <- AuthorizationServer: (3) Authorization Code
LegitimateApp <- Browser: (4) Authorization Code
|||
MaliciousApp <-- Browser: (4) Authorization Code
MaliciousApp --> AuthorizationServer: (5) Authorization Grant
MaliciousApp <-- AuthorizationServer: (6) Access Token
@enduml

詳細說明如下:

  1. 合法 App 在發出授權請求時,因為像智慧型手機的 redirect_uri 只能設定成自定義 URI 才能順利引導使用者回到 App 裡
  2. 因 OAuth2 定義需要 TLS 加密保護,所以這裡是安全的
  3. 拿到關鍵的 code 了
  4. 合法 App 取得 code,但如果有另一個惡意 App 也註冊了相同的 URI,則惡意 App 也能拿得到 code
  5. 只要惡意 App 取得了 code,即可對授權伺服器發出要 token 的請求
  6. 最後 token 就會被惡意的 App 偷走了

從流程圖上來看,這個攻擊是非常容易實現的,而 RFC 7636 正是定義如何防範此問題。

首先先來定義專有名詞:

  • code verifier: 一個隨機字串,與授權請求與 token 請求互相關聯
  • code challenge: 從 code verifier 產生的 challenge,會夾帶到授權請求一起送出,以便後續驗證
  • code challenge method: 產生 code challenge 的方法

在 Client 發出的授權請求前,先保存一個參數在儲存空間裡,稱之為 code verifier,值為一個密碼學安全的僞隨機數。接著 Client 使用 code_verifier 搭配 code challenge method 產生另一個值稱之為 code challenge。方法 RFC 裡定義有兩種:

  • plain: 不做任何處理
  • S256: 將 code verifier 做 SHA-256 雜湊後再做 Base64UrlEncode

在發送授權請求的時候,多定義了下面這兩個參數:

名稱必要
code_challengeREQUIRED
code_challenge_methodOPTIONAL

授權伺服器在收到 code_challenge 時,必須將它與回應的 code 做關聯。

接著在 Client 收到 code 後,發出 token 請求時,多帶 code verifier:

名稱必要
code_verifierREQUIRED

授權伺服器收到 code_verifiercode 之後,先把 code 關聯的 code_challengecode_challenge_method 找到後,使用 code_verifier 搭配關聯到的 code_challenge_method 產生 challenge,再跟 code_challenge 比較,即可知道發出 token 請求的 Client 與當初發授權請求的是同一個 Client。

@startuml
participant LegitimateApp
participant MaliciousApp
participant Browser
participant AuthorizationServer
LegitimateApp -> LegitimateApp: (1) Generate code_verifier and code_challenge
LegitimateApp -> Browser: (1) Authorization Request with code_challenge
Browser -> AuthorizationServer: (2) Authorization Request with code_challenge
AuthorizationServer -> AuthorizationServer: (2) Binding Code and code_challenge
Browser <- AuthorizationServer: (3) Authorization Code
LegitimateApp <- Browser: (4) Authorization Code
LegitimateApp -> AuthorizationServer: (4) Authorization Grant with code_verifier
LegitimateApp <- AuthorizationServer: (4) Access Token
|||
MaliciousApp <-- Browser: (4) Authorization Code
MaliciousApp --> AuthorizationServer: (5) Authorization Grant without code_verifier
MaliciousApp <-- AuthorizationServer: (6) Error Response
@enduml

這裡的步驟跟前面說明的一樣,主要是第 5 步,因為即便惡意 App 拿到了 code_challenge,也無法回推第 5 步要傳給授權服務器的 code_verifier,因此即能有效阻檔「授權碼攔截攻擊」