透過 OAuth 2.0 進行使用者驗證

The OAuth 2.0 規格制定了一個「委任」協定,可有效傳達 Web 啟用的應用程式和 API 網路中的「授權決定」。OAuth 運用於各式各樣的應用程式,包含提供使用者驗證機制。這讓許多開發人員和 API 供應商錯誤地得出 OAuth 本身就是「驗證」協定,並誤將其用於此目的。再次說明,以求明確

OAuth 2.0 不是驗證協定。

很多混淆來自於 OAuth 被用於驗證協定「內部」的事實,而且開發人員會看到 OAuth 成分,並且與 OAuth 流程互動,並假設僅透過使用 OAuth,就能達成使用者驗證。結果證明不只不正確,且對服務提供者、開發人員和最終使用者來說也很危險。

本文旨在協助潛在的「身分提供者」解決如何建置驗證和身分 API,並以 OAuth 2.0 為基礎。實際上,如果你的說法是「我有 OAuth 2.0,且需要驗證和身分」,那就继续阅读。

何謂驗證?

當使用者存取應用程式時,驗證會告訴應用程式目前的使用者是誰,以及他們是否現身。完整的驗證協定可能還會告訴你許多關於此使用者的屬性,例如唯一識別碼、電子郵件地址,以及使用者在應用程式中看到「Good Morning」時,該如何稱呼他們。驗證與使用者和他們在應用程式中的現身有關,而網際網路規模的驗證協定需要能在網路和安全性邊界中做到這一點。

然而,OAuth 跟該應用程式一概沒說。OAuth 對於使用者完全隻字未提,也沒有說明使用者如何證明身分,或甚至使用者是否還在場。從 OAuth 客戶端來看,它要求 token、取得 token,然後最終使用該 token 來存取某些 API。它對於授權這個應用程式的是誰,或甚至是否真的有使用者的存在一無所知。事實上,OAuth 的重點在於針對在客戶端與它要存取資源之間的連線中沒有使用者的情況提供此委派存取權限。這對於客戶端授權來說很棒,但對於需要找出使用者是否在場(以及身分為何)的驗證來說卻很糟糕。

對於我們的主題,還有一個額外混淆點,即 OAuth 處理程序通常包含數種驗證類型:資源擁有者在授權階段對授權伺服器進行驗證,客戶端在 token 終點對授權伺服器進行驗證,其他情況可能還有。OAuth 協定中這些驗證事件的存在並不代表 OAuth 協定本身就能可靠地傳達驗證。

雖然它確實如此,某些事物可以與 OAuth 結合使用,以在此委派及授權協定之上建立驗證及身分協定。在幾乎所有情況下,OAuth 的核心功能都保持不變,而發生的事情是使用者會將存取他們的身分的權限委派給他們嘗試登入的應用程式。接著,客戶端應用程式就會變成身分 API 的使用者,從而發現當初授權客戶端的使用者是誰。以這種方式在授權之上建立驗證的一個主要好處是它允許管理最終使用者同意,這在網際網路規模的跨網域身分聯盟中非常重要。另一個重要好處是使用者可以在同時將存取權限連同其身分委派給其他受保護的 API,讓應用程式開發人員和最終使用者都更方便管理。透過一次呼叫,應用程式就能找出使用者是否已登入、應用程式應該如何稱呼使用者、下載相片來列印,以及在他們的訊息串中張貼更新。這種簡便性非常吸引人,但藉由同時進行這兩件事,許多開發人員會將這兩個功能混為一談。

驗證相對於授權:一個隱喻

為了幫忙澄清問題,不妨用一個比喻來想:巧克力比對布朗尼。一開始,兩者的性質就很不一樣:巧克力是食材,布朗尼是糖果。巧克力可以用來製作許多不同東西,甚至可以單獨食用。布朗尼可以用許多不同東西製作,其中一項可能是巧克力,但是製作布朗尼需要的材料不只一種,而巧克力甚至不是必要的。因此,說巧克力等於布朗尼是不正確的,而說巧克力等於巧克力布朗尼更是過於牽強。

在本比喻中,OAuth 就是巧克力。這是一種具有多功能性的食材,對許多不同事物來說至關重要,甚至可以單獨使用,發揮很棒的效果。而驗證更像是布朗尼。這種食材需要至少幾種材料正確組合才能發揮作用,而 OAuth 可能會是其中一種食材(也許是最重要的食材),但它不一定會用到。你需要一個組合食材以及組合順序的食譜,而且有許多不同的食譜說明如何組合完成。

事實上,有很多眾所周知的食譜可以拿來處理特定供應商,例如 Facebook Connect、Twitter 登入和 OpenID Connect(支援 Google 的登入系統以及其他登入系統)。這些食譜各自新增許多項目,例如共通的檔案 API,到 OAuth 中,以創造驗證協定。你能在不使用 OAuth 的情況下建立驗證協定嗎?當然可以,市面上有很多種,就像有很多種不加巧克力的布朗尼可供選擇。但是我們今天要討論的主題明確是建立在 OAuth 2.0 基礎上的驗證,哪些地方可能出問題,以及如何達成安全和美味的狀態。

使用 OAuth 進行驗證的常見陷阱

即使非常有可能使用 OAuth 來建立驗證協定,但是有些人很容易會因此絆倒,可能是身分供應商那邊,也可能是身分使用者那邊。本文說明的做法旨在讓潛在的身分供應商了解常見風險,並讓使用者了解在使用基於 OAuth 的驗證系統時,哪些常見錯誤可以避免。

將存取權杖當成驗證證明

由於驗證通常發生在存取權杖核發之前,因此很容易考慮接收任何類型的存取權杖,證明已執行此類驗證。不過,隻擁有存取權杖本身並無法傳達任何訊息給客戶端。在 OAuth 中,權杖設計為對客戶端不透明,但在使用者驗證時,客戶端需要能夠從權杖中取得一些資訊。

此問題源於客戶端並非 OAuth 存取權杖的預計 受眾。反而是該權杖的 授權出示者,而 受眾 實際上是受保護的資源。受保護的資源通常不會透過權杖本身就能判斷使用者是否仍然存在,因為根據 OAuth 協定的性質和設計,使用者將無法在客戶端和受保護資源之間的連線上。為了解決此問題,需要製作一個直接提供給客戶端本身的製品。這可以使用存取權杖的雙重用途,定義客戶端可以解析和理解的格式來完成。不過,由於通用 OAuth 並未定義存取權杖本身的特定格式或結構,因此,諸如 OpenID Connect 的 ID 權杖和 Facebook Connect 的已簽署回應等協定會在存取權杖旁邊提供次要權杖,直接將驗證資訊傳達給客戶端。這允許主要存取權杖保持對客戶端不透明,就像一般的 OAuth 一樣。

視為驗證憑證的受保護 API 的存取

由於存取權杖可以與一組使用者屬性進行交易,因此很容易認為持有有效的存取權杖足以證明使用者已通過驗證。這種假設在某些情況下被證實是正確的,在這種情況下,會在授權伺服器對使用者進行驗證的情況下新鑄造權杖。不過,這並不是在 OAuth 中取得存取權杖的唯一方法。可以在使用者不在場的情況下使用更新權杖和規則來取得存取權杖,在某些情況下,存取授權甚至不需要使用者進行驗證。

此外,使用者不再存在後,存取權杖通常仍然可用很長一段時間。請記住,由於 OAuth 是委派協定,因此這對其設計至關重要。換言之,如果客戶端要確定驗證仍然有效,則僅再次將權杖與使用者的屬性進行交易並不足夠,因為受 OAuth 保護的資源(即身分識別 API)通常沒有辦法判斷使用者是否存在。

插入存取權杖

當客戶端從代幣端點的回呼呼叫以外的來源接受存取權杖時,會發生其他(而且非常危險的)威脅。這可能會發生在使用隱式流程(代幣直接傳遞為 URL hash 中的參數)且未正確使用 OAuth state 參數的客戶端上。如果應用程式的不同部分傳遞存取權杖以在元件之間「分享」存取,也會發生此問題。這樣做有問題,因為它為外部協力廠商提供潛在注入存取權杖到應用程式(而且潛在會外洩應用程式以外)的途徑。如果客戶端應用程式未透過特定機制驗證存取權杖,則無法分辨有效代幣和攻擊代幣之間的差異。

這可以用授權碼流程來減輕,而且只直接從授權伺服器的代幣端點接受代幣,並使用攻擊者無法猜測的 state 值。

缺少受眾限制

另一個與交易存取權杖以取得一組屬性以取得目前使用者的問題是,大多數 OAuth API 皆不提供受眾限制的機制以取得傳回的資訊。換句話說,極有可能讓新手客戶端使用其他客戶端的(有效)代幣,並讓新手客戶端將此當作「登入」事件。畢竟,代幣有效而且呼叫 API 會傳回有效的使用者資訊。當然,問題在於,使用者未執行任何動作來證明自己出現,而且在這個情況下,他們甚至未授權新手客戶端。

這個問題可以用將身分驗證資訊傳達給客戶端的方式來減輕,而身分驗證資訊會搭配客戶端能夠辨識和驗證的身分識別碼,讓客戶端區分是為自己身分驗證,還是為其他應用程式身分驗證。這也能透過在 OAuth 程序期間直接傳遞一組身分驗證資訊到客戶端來緩解,而非透過二次機制(例如受 OAuth 保護的 API),阻止客戶端在程序中後續時間具有未知和不可信賴的注入資訊組。

注入無效使用者資訊

如果攻擊者能夠攔截或合作客戶端其中一個呼叫,他們就能變更傳回使用者資訊的內容,而且客戶端無法察覺到任何異狀。這將允許攻擊者透過在正確的呼叫順序中置換使用者識別碼讓新手客戶端冒充使用者。這可以用在身分驗證程序處理期間(例如在 OAuth 代幣旁)直接從身分識別供應商取得身分驗證資訊,並透過可驗證簽章保護身分驗證資訊的方式來減輕。

每個潛在身分識別供應商都有不同的程序

基於 OAuth 的身分識別 API 最大的問題之一是,即使使用完全符合標準的 OAuth 機制,不同的提供者必然會以不同的方式實現實際的身分識別 API 的詳細資料。例如,某個提供者的使用者的識別碼可以出現在 `user_id` 欄位中,但在另一個提供者中則出現在 `subject` 欄位中。即使這些在語義上是相等的,它們仍然需要兩個分開的程式碼路徑才能處理。換句話說,即使授權出現在每個提供者的方式相同,身分驗證資訊的傳達也可能不同。這個問題可以透過提供者使用建構在 OAuth 上的標準「身分驗證協定」來減輕,這樣一來無論身分識別資訊從何而來,都是透過相同的方式傳輸。

之所以會發生這個問題,是因為在這裡討論用於傳達身分驗證資訊的機制清楚地遺漏在 OAuth 的範圍之外。OAuth 定義沒有特定令牌格式,定義沒有共用的範圍集對於存取令牌,並且完全沒有處理受保護資源如何驗證存取令牌

使用 OAuth 的使用者身分驗證標準:OpenID Connect

OpenID Connect 是 2014 年初發布的開放式標準,定義了使用 OAuth 2.0 進行使用者身分驗證的可互操作方式。從本質上來說,它是一個廣為發布的「巧克力软糖食譜」,已經過許多專家的試驗和測試。應用程式可以對許多提供者使用一種協定來工作,而不是針對每個潛在的身分識別提供者建構不同的協定。由於它是開放式標準,任何人都可以在沒有限制或智慧財產權考量的情況下實作 OpenID Connect。

OpenID Connect 直接建構在 OAuth 2.0 上,而且在大部分情況下會與 (或建構在) OAuth 基礎設施一起佈署。OpenID Connect 亦使用 JSON 物件簽署及加密 (JOSE) 規範套件在不同的地方攜帶已簽署及加密的資訊。事實上,具有 JOSE 功能的 OAuth 2.0 佈署已經邁向定義一個完全相容的 OpenID Connect 系統一大步,這兩者之間的差異很小。但這個差異造成了很大的不同,而 OpenID Connect 能夠透過將幾個關鍵組件新增到 OAuth 資源來避免上述討論的許多陷阱

ID 令牌

OpenID Connect ID Token 是一個已簽署的 JSON 網路代幣 (JWT),會連同一般 OAuth 存取權杖提供給客戶端應用程式。ID Token 包含一組關於驗證階段的宣告,包括使用者的識別碼 (sub)、頒發代幣的身分提供者的識別碼 (iss) 以及此代幣所創建的客戶端識別碼 (aud)。此外,ID Token 還包含關於代幣的有效 (且通常為短暫的) 使用期限的資訊,以及任何要傳達給客戶端關於驗證內容的資訊,例如:使用者上次使用主要驗證機制的時間點。由於客戶端了解 ID Token 的格式,因此可以自行剖析代幣內容並取得此資訊,而無需依賴外部服務。此外,它會連同 (而非取代) 存取權杖一起發出,讓客戶端能保持存取權杖的不透明,就像它在一般 OAuth 中定義的那樣。最後,代幣本身是由身分提供者的私密金鑰簽署,除了最初用於取得代幣的 TLS 傳輸保護之外,還能為裡面的宣告增加一層保護層,防止各種冒充攻擊。客戶端只要對此 ID 代幣進行幾個簡單檢查,就能保護自己免於遭到許多常見攻擊。

由於 ID Token 已由授權伺服器簽署,因此它也提供一個位置,可以用來增加授權代碼 (c_hash) 和存取權杖 (at_hash) 的獨立簽章。客戶端可以在維持授權代碼與存取權杖內容對客戶端不透明的情況下,驗證這些雜湊,以防止各種注入攻擊。

使用者資訊端點

請注意,客戶端並不需要使用存取權杖,因為 ID Token 包含處理驗證事件的所有必要資訊。不過,為了提供與 OAuth 的相容性,並符合平行授權身分和其他 API 存取的普遍趨勢,OpenID Connect 始終會連同 OAuth 存取權杖一起發出 ID 代幣。

除了身分識別代幣 (ID Token) 中的主張外, OpenID Connect 還定義了一個包含目前使用者主張的標準保護資源。如上所述,這些主張並不屬於驗證程序的一部分,而是提供成批身分屬性,讓應用程式開發人員更能發揮驗證協定的價值。畢竟,「早上好,王小美」聽起來比「早上好,9XE3-JI34-00132A」要好得多。OpenID Connect 定義了一組標準的 OAuth 範圍,用於對應這些屬性的子集:個人資料電子郵件電話號碼地址,讓純 OAuth 授權要求能傳送要求所需的必要資訊。OpenID Connect 定義了一個特殊的 openid 範圍,用來啟用身分識別代幣的發行,並由存取權杖來存取使用者資訊端點。OpenID Connect 範圍可用於與其他非 OpenID Connect 的 OAuth 範圍並用,而不會產生衝突,而所發行的存取權杖可能同時針對不同的保護資源。這讓 OpenID Connect 身分系統能與 OAuth 授權系統順利共存。

動態伺服器發現和用戶端註冊

OAuth 2.0 是為了允許各種不同的部署而撰寫,但根據設計並沒有明確說明這些部署如何進行設定,或各組成如何互相識別。這在一般的 OAuth 世界中是可以接受的,其中一個授權伺服器會保護特定 API,而且兩者緊密結合。透過 OpenID Connect,一個常見的受保護 API 可以部署在各種用戶端和提供者中,而這些用戶端和提供者都需要互相認識才能運作。要讓每個用戶端都能事先了解每個提供者並不可行,而要讓每個提供者都知道每個潛在用戶端更是不可行。

為了對抗這種問題,OpenID Connect 定義了一個 發現 協定,讓客戶端可以輕鬆取得如何與特定身分提供者互動的資訊。在這項交易的另一端,OpenID Connect 定義了一個 用戶端註冊 協定,讓用戶端可以引介給新的身分提供者。透過使用這兩個機制和共用的身分識別 API,OpenID Connect 能在網際網路規模下運作,而各方都不必事先互相了解。

與 OAuth 2.0 的相容性

即使具備所有這些強大的驗證功能,OpenID Connect(根據設計)仍然與純 OAuth 2.0 相容,這讓它成為部署在 OAuth 系統上的一個絕佳選擇,而且開發人員的負擔很小。事實上,如果某項服務已經在使用 OAuth 和 JSON 物件簽署和加密 (JOSE) 規格(包括 JWT),該服務其實已經能支援 OpenID Connect 了。

為了便於建置完善的用戶端應用程式,OpenID Connect 作業小組已針對使用授權碼流程建置 基本 OpenID Connect 用戶端,以及建置 暗示式 OpenID Connect 用戶端 發布文件。兩份文件皆引導開發人員建構基礎 OAuth 2.0 用戶端,並加入 OpenID Connect 所需的少數元件。

進階功能

儘管核心規格相當簡潔,但並非所有使用案例都能透過基礎機制妥善解決。為了支援包含高安全性佈署的進階使用案例,OpenID Connect 也定義了多項進階功能選項,超越標準的 OAuth,包含以下內容(以及其他內容)

延伸資料