Proof Key for Code Exchange

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

Authorization Code Interception Attack

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 正是定義如何防範此問題。

Proof Key for Code Exchange

首先先來定義專有名詞:

  • 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_challenge REQUIRED
code_challenge_method OPTIONAL

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

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

名稱 必要
code_verifier REQUIRED

授權伺服器收到 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,因此即能有效阻檔「授權碼攔截攻擊」

參考資料