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 的網頁版則不屬於。
解決什麼問題
Section titled “解決什麼問題”原生應用程式屬於 Public Client——無法安全地存放 client_secret,因為反編譯應用程式並取出密碼是做得到的。這個特性讓原生應用程式在執行 OAuth 2.0 授權時,需要特別的處理方式。
External vs Embedded User-Agent
Section titled “External vs Embedded User-Agent”RFC 8252 定義了兩種使用者代理:
- 外部使用者代理(External User-Agent):獨立的應用程式,通常是系統預設瀏覽器(如 iOS 上的 Safari)。關鍵特徵是它與原生應用程式是不同的程序。
- 嵌入式使用者代理(Embedded User-Agent):將瀏覽器元件嵌入應用程式畫面內(如 Android 的 WebView、iOS 的 WKWebView),讓應用程式內直接呈現網頁。
為什麼不該使用嵌入式瀏覽器
Section titled “為什麼不該使用嵌入式瀏覽器”早期的 OAuth 2.0 協定(RFC 6749)提到可以使用嵌入式瀏覽器來提供更好的使用者體驗,但 RFC 8252 明確指出嵌入式瀏覽器存在以下問題:
- 使用者無法辨識網站安全性——外部瀏覽器通常會顯示網址列和 TLS 憑證狀態,幫助使用者辨識是否為合法網站。嵌入式瀏覽器通常看不到這些資訊,攻擊者可以輕易建立假的授權頁面進行釣魚攻擊,使用者完全無法察覺。
- 應用程式可存取使用者憑證——原生應用程式有能力透過嵌入式瀏覽器存取使用者輸入的帳號密碼、Cookie 及所有可監控的資料。這違反了 OAuth 2.0 「不讓使用者憑證暴露給第三方」的設計初衷。
- 狀態獨立導致重複驗證——嵌入式瀏覽器的狀態是獨立的,不同應用程式連到同一個授權伺服器時,使用者必須重新登入,造成不必要的體驗摩擦。
授權平台已開始阻擋嵌入式瀏覽器
Section titled “授權平台已開始阻擋嵌入式瀏覽器”主要的授權平台已經開始實際阻擋嵌入式瀏覽器的 OAuth 請求:
- Google:自 2021 年起逐步實施,2023 年 2 月起全面阻擋嵌入式 WebView 的 OAuth 請求,偵測到時回傳
disallowed_useragent錯誤。Google 明確指出嵌入式瀏覽器可以充當中間人——攔截網路請求、注入腳本記錄鍵盤輸入、存取 session cookie。 - Facebook(Meta):自 2021 年 10 月起阻擋 Android 嵌入式瀏覽器的登入請求,原因是偵測到越來越多透過嵌入式瀏覽器進行的釣魚攻擊。
外部瀏覽器的使用者體驗
Section titled “外部瀏覽器的使用者體驗”使用外部瀏覽器意味著使用者在授權過程中會被跳出應用程式、切換到瀏覽器,完成授權後再切回來——這在行動裝置上體驗並不理想。但目前 Android 與 iOS 都已提供 API,能在不切換應用程式的情況下啟動外部瀏覽器,同時兼顧安全性與使用者體驗。
如何解決與取捨
Section titled “如何解決與取捨”RFC 8252 定義的最佳實踐做法是:使用外部瀏覽器執行授權碼流程,搭配 PKCE 保護授權碼。流程如下:
- 應用程式啟動外部瀏覽器,發送授權請求(帶
code_challenge) - 使用者在外部瀏覽器中完成身分驗證與授權
- 授權伺服器透過預先註冊的 URI 將授權碼導回應用程式
- 應用程式以授權碼搭配
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
三種 Redirect URI 註冊方式
Section titled “三種 Redirect URI 註冊方式”Web 應用程式透過 URI 開啟,所以設定 redirect_uri 很直覺。但原生應用程式的啟動方式不同——使用者是透過點擊圖示或指令來開啟,沒有 URI 的概念。RFC 8252 定義了三種方式讓外部瀏覽器能將授權回應導回應用程式:
HTTPS URL 比對(推薦)
Section titled “HTTPS URL 比對(推薦)”iOS 與 Android 都允許應用程式註冊 HTTPS URL,當外部瀏覽器開啟符合註冊的 URL 時,系統會啟動對應的應用程式。例如:點擊 Discord 的連結時啟動 Discord App。
這是 RFC 8252 建議的方案,因為它提供良好的容錯性——如果平台不支援這個方法,使用者還是可以在外部瀏覽器中透過合理的返回網址繼續操作。
自定義 URL Scheme
Section titled “自定義 URL Scheme”部分平台允許應用程式註冊自定義的 URL Scheme,在瀏覽器開啟具有該 Scheme 的 URL 時啟動對應的應用程式。例如 JetBrains Toolbox 使用 jetbrains://toolbox/jba/auth 作為 redirect URI。
這個方法的缺點是沒有全域註冊表——不同的應用程式可能使用相同的 Scheme 而產生衝突。建議使用反向域名來降低碰撞風險:
原始域名:app.mileschou.me反向作為 Scheme:me.mileschou.app://Loopback URL
Section titled “Loopback URL”在本機隨機 port 上啟動 HTTP 服務來接收授權回應,通常用於桌面應用程式或 CLI 應用程式。範例:
http://127.0.0.1:49152/redirect這個方式有兩個需要注意的地方:
- Port 必須隨機——無法預先知道使用者裝置上哪些 port 已被佔用
- 授權伺服器必須忽略 port 比對——因為 port 是隨機的,無法與註冊時完全一致。授權伺服器遇到 loopback URL 時,不管授權請求傳了什麼 port 都應該接受
在這個情境下使用 HTTP 而非 HTTPS 是可以接受的,因為請求只在本機轉導,不會離開使用者的裝置。
以 JetBrains Toolbox 的授權請求為例,可以看到自定義 URL Scheme 與 PKCE 的組合:
| 參數 | 值 |
|---|---|
client_id | toolbox |
response_type | code |
scope | offline_access openid r_assets r_ide_auth |
redirect_uri | jetbrains://toolbox/jba/auth |
state | 52b6d2f2-3d02-48c6-a4f1-1073a93b7a7f |
code_challenge | gfMN3iktOCyTKu2xYjFdlbApjqtTI0wK8QfA8m5zjuQ |
code_challenge_method | S256 |
這裡可以看到:redirect_uri 使用了自定義 URL Scheme jetbrains://,同時帶有 code_challenge 與 code_challenge_method,代表使用了 PKCE。
授權碼攔截攻擊
Section titled “授權碼攔截攻擊”使用自定義 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 如何阻擋
Section titled “PKCE 如何阻擋”搭配 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 的碰撞風險
Section titled “自定義 URL Scheme 的碰撞風險”自定義 URL Scheme 的根本問題在於沒有全域註冊表——任何應用程式都可以註冊任意的 Scheme,平台也沒有機制防止碰撞。這也是為什麼 HTTPS URL 比對是更安全的選擇:HTTPS URL 需要驗證域名的所有權,確保只有合法的應用程式能接收授權回應。
使用外部瀏覽器是唯一推薦的做法
Section titled “使用外部瀏覽器是唯一推薦的做法”綜合上述所有安全考量——嵌入式瀏覽器的釣魚風險、憑證洩漏風險,加上主要授權平台已實際阻擋嵌入式瀏覽器——使用外部瀏覽器搭配 PKCE 是目前 RFC 8252 唯一推薦的授權方式。