跳到內容

OAuth 2.0 for Native Apps

RFC 8252 是 OAuth 2.0 針對原生應用程式(Native Apps)制定的最佳實踐(Best Current Practice,BCP)。原生應用程式指的是安裝在使用者設備上的應用程式,包括:

  • 行動裝置上的應用程式(Android、iOS)
  • 桌面軟體(Windows、macOS、Linux)
  • 基於 Web 技術但作為桌面軟體發布的應用程式(如 Electron)

舉例來說,Notion 發布在 Android 與 iOS 上的 App 以及桌面軟體都屬於原生應用程式,但 Notion 的網頁版則不屬於。

原生應用程式屬於 Public Client——無法安全地存放 client_secret,因為反編譯應用程式並取出密碼是做得到的。這個特性讓原生應用程式在執行 OAuth 2.0 授權時,需要特別的處理方式。

RFC 8252 定義了兩種使用者代理:

  • 外部使用者代理(External User-Agent):獨立的應用程式,通常是系統預設瀏覽器(如 iOS 上的 Safari)。關鍵特徵是它與原生應用程式是不同的程序
  • 嵌入式使用者代理(Embedded User-Agent):將瀏覽器元件嵌入應用程式畫面內(如 Android 的 WebView、iOS 的 WKWebView),讓應用程式內直接呈現網頁。

早期的 OAuth 2.0 協定(RFC 6749)提到可以使用嵌入式瀏覽器來提供更好的使用者體驗,但 RFC 8252 明確指出嵌入式瀏覽器存在以下問題:

  1. 使用者無法辨識網站安全性——外部瀏覽器通常會顯示網址列和 TLS 憑證狀態,幫助使用者辨識是否為合法網站。嵌入式瀏覽器通常看不到這些資訊,攻擊者可以輕易建立假的授權頁面進行釣魚攻擊,使用者完全無法察覺。
  2. 應用程式可存取使用者憑證——原生應用程式有能力透過嵌入式瀏覽器存取使用者輸入的帳號密碼、Cookie 及所有可監控的資料。這違反了 OAuth 2.0 「不讓使用者憑證暴露給第三方」的設計初衷。
  3. 狀態獨立導致重複驗證——嵌入式瀏覽器的狀態是獨立的,不同應用程式連到同一個授權伺服器時,使用者必須重新登入,造成不必要的體驗摩擦。

授權平台已開始阻擋嵌入式瀏覽器

Section titled “授權平台已開始阻擋嵌入式瀏覽器”

主要的授權平台已經開始實際阻擋嵌入式瀏覽器的 OAuth 請求:

  • Google:自 2021 年起逐步實施,2023 年 2 月起全面阻擋嵌入式 WebView 的 OAuth 請求,偵測到時回傳 disallowed_useragent 錯誤。Google 明確指出嵌入式瀏覽器可以充當中間人——攔截網路請求、注入腳本記錄鍵盤輸入、存取 session cookie。
  • Facebook(Meta):自 2021 年 10 月起阻擋 Android 嵌入式瀏覽器的登入請求,原因是偵測到越來越多透過嵌入式瀏覽器進行的釣魚攻擊。

使用外部瀏覽器意味著使用者在授權過程中會被跳出應用程式、切換到瀏覽器,完成授權後再切回來——這在行動裝置上體驗並不理想。但目前 Android 與 iOS 都已提供 API,能在不切換應用程式的情況下啟動外部瀏覽器,同時兼顧安全性與使用者體驗。

RFC 8252 定義的最佳實踐做法是:使用外部瀏覽器執行授權碼流程,搭配 PKCE 保護授權碼。流程如下:

  1. 應用程式啟動外部瀏覽器,發送授權請求(帶 code_challenge
  2. 使用者在外部瀏覽器中完成身分驗證與授權
  3. 授權伺服器透過預先註冊的 URI 將授權碼導回應用程式
  4. 應用程式以授權碼搭配 code_verifier 向授權伺服器換取 Access Token
sequenceDiagram
    participant App as 原生應用程式
    participant Browser as 外部瀏覽器
    participant AS as 授權伺服器

    App ->> App: 產生 code_verifier 與 code_challenge
    App ->> Browser: 啟動瀏覽器,發送授權請求(帶 code_challenge)
    Browser ->> AS: 授權請求
    AS ->> AS: 身分驗證與授權
    AS ->> Browser: 回傳授權碼(透過 redirect URI)
    Browser ->> App: 透過註冊的 URI 啟動應用程式,帶回授權碼
    App ->> AS: Token 請求(帶 code_verifier)
    AS ->> AS: 驗證 code_verifier 與 code_challenge
    AS ->> App: 回傳 Access Token

Web 應用程式透過 URI 開啟,所以設定 redirect_uri 很直覺。但原生應用程式的啟動方式不同——使用者是透過點擊圖示或指令來開啟,沒有 URI 的概念。RFC 8252 定義了三種方式讓外部瀏覽器能將授權回應導回應用程式:

iOS 與 Android 都允許應用程式註冊 HTTPS URL,當外部瀏覽器開啟符合註冊的 URL 時,系統會啟動對應的應用程式。例如:點擊 Discord 的連結時啟動 Discord App。

這是 RFC 8252 建議的方案,因為它提供良好的容錯性——如果平台不支援這個方法,使用者還是可以在外部瀏覽器中透過合理的返回網址繼續操作。

部分平台允許應用程式註冊自定義的 URL Scheme,在瀏覽器開啟具有該 Scheme 的 URL 時啟動對應的應用程式。例如 JetBrains Toolbox 使用 jetbrains://toolbox/jba/auth 作為 redirect URI。

這個方法的缺點是沒有全域註冊表——不同的應用程式可能使用相同的 Scheme 而產生衝突。建議使用反向域名來降低碰撞風險:

原始域名:app.mileschou.me
反向作為 Scheme:me.mileschou.app://

在本機隨機 port 上啟動 HTTP 服務來接收授權回應,通常用於桌面應用程式或 CLI 應用程式。範例:

http://127.0.0.1:49152/redirect

這個方式有兩個需要注意的地方:

  1. Port 必須隨機——無法預先知道使用者裝置上哪些 port 已被佔用
  2. 授權伺服器必須忽略 port 比對——因為 port 是隨機的,無法與註冊時完全一致。授權伺服器遇到 loopback URL 時,不管授權請求傳了什麼 port 都應該接受

在這個情境下使用 HTTP 而非 HTTPS 是可以接受的,因為請求只在本機轉導,不會離開使用者的裝置。

以 JetBrains Toolbox 的授權請求為例,可以看到自定義 URL Scheme 與 PKCE 的組合:

參數
client_idtoolbox
response_typecode
scopeoffline_access openid r_assets r_ide_auth
redirect_urijetbrains://toolbox/jba/auth
state52b6d2f2-3d02-48c6-a4f1-1073a93b7a7f
code_challengegfMN3iktOCyTKu2xYjFdlbApjqtTI0wK8QfA8m5zjuQ
code_challenge_methodS256

這裡可以看到:redirect_uri 使用了自定義 URL Scheme jetbrains://,同時帶有 code_challengecode_challenge_method,代表使用了 PKCE

使用自定義 URL Scheme 時,惡意應用程式可以註冊相同的 Scheme。當授權伺服器回傳授權碼時,惡意應用程式也能收到同樣的授權碼,進而嘗試換取 Access Token:

sequenceDiagram
    participant User Agent
    participant 合法應用程式
    participant 授權伺服器
    participant 惡意應用程式
    合法應用程式 ->> User Agent: 1. 發送授權請求
    User Agent ->> 授權伺服器: 2. 身分驗證與授權
    授權伺服器 ->> User Agent: 3. 回傳授權回應
    User Agent ->> 合法應用程式: 4. 發送授權回應
    note left of 惡意應用程式: 如果被惡意應用程式攔截到 Code
    User Agent -->> 惡意應用程式: 4. 發送授權回應
    惡意應用程式 -->> 授權伺服器: 5. 請求 Access Token
    授權伺服器 -->> 惡意應用程式: 6. 取得 Access Token

搭配 PKCE 後,即使惡意應用程式攔截到授權碼,因為沒有 code_verifier 就無法通過授權伺服器的驗證:

sequenceDiagram
    participant User Agent
    participant 合法應用程式
    participant 授權伺服器
    participant 惡意應用程式
    合法應用程式 ->> 合法應用程式: 1. 產生 code_verifier 與 code_challenge
    合法應用程式 ->> User Agent: 1. 帶有 code_challenge 的授權請求
    User Agent ->> 授權伺服器: 2. 帶有 code_challenge 的授權請求
    授權伺服器 ->> 授權伺服器: 2. 綁定授權碼和 code_challenge
    授權伺服器 ->> User Agent: 3. 回傳授權回應
    User Agent ->> 合法應用程式: 4. 發送授權回應
    合法應用程式 ->> 授權伺服器: 4. 請求 Access Token(帶 code_verifier)
    授權伺服器 ->> 合法應用程式: 4. 取得 Access Token
    note right of User Agent: 如果被惡意應用程式攔截到 Code
    User Agent -->> 惡意應用程式: 4. Authorization Code
    惡意應用程式 -->> 授權伺服器: 5. 請求 Access Token(沒有 code_verifier)
    授權伺服器 -->> 惡意應用程式: 6. 錯誤回應

自定義 URL Scheme 的根本問題在於沒有全域註冊表——任何應用程式都可以註冊任意的 Scheme,平台也沒有機制防止碰撞。這也是為什麼 HTTPS URL 比對是更安全的選擇:HTTPS URL 需要驗證域名的所有權,確保只有合法的應用程式能接收授權回應。

使用外部瀏覽器是唯一推薦的做法

Section titled “使用外部瀏覽器是唯一推薦的做法”

綜合上述所有安全考量——嵌入式瀏覽器的釣魚風險、憑證洩漏風險,加上主要授權平台已實際阻擋嵌入式瀏覽器——使用外部瀏覽器搭配 PKCE 是目前 RFC 8252 唯一推薦的授權方式。