「應用安全」OAuth和OpenID Connect的全面比較

2019-09-01     首席架構師

1.簡介

在這篇文章中,從頭開始實施OAuth 2.0和OpenID Connect伺服器的開發人員(我)討論了調查結果。基本上,實施的考慮點是在討論中寫出來的。因此,對於那些正在尋找「如何及時設置OAuth 2.0和OpenID Connect伺服器」等信息的人來說,這不是一個文檔。如果您正在尋找此類信息,請訪問GitHub上的java-oauth-server和java-resource-server。使用這些,您可以在10分鐘內啟動授權伺服器和資源伺服器,發出訪問令牌並使用訪問令牌調用We​​b API,而無需設置資料庫伺服器。

偏見

我是Authlete,Inc。的聯合創始人,該公司是一家在雲端提供OAuth 2.0和OpenID Connect實施的公司,因此本文檔可能會受到這種偏見的影響。因此,請在腦海中閱讀本文檔。但是,基本上,我將從純工程師的角度來寫這篇文章。


2.OAuth是否必要?

「我們希望在我們的公司網站上這樣做。我們應該實施OAuth嗎?「 - 這經常被問到。從本質上講,這個問題是詢問OAuth是什麼。

我經常用來解釋OAuth的一句話答案如下。

OAuth 2.0是一種框架,其中服務的用戶可以允許第三方應用程式訪問他/她在服務中託管的數據,而無需向應用程式透露他/她的憑據。


重要的一點是「不向第三方應用程式透露憑據」。 OAuth就是為此而存在的。一旦理解了這一點,您可以通過檢查是否滿足以下條件來判斷您是否應該為公司的服務準備OAuth伺服器。

  1. 您的服務管理用戶的數據。
  2. 您希望第三方為您的服務用戶開發應用程式。
  3. 您不希望向第三方開發的應用程式透露用戶憑據。

即使上述條件不滿足且貴公司服務的應用程式僅為自制服務,如果您可能希望第三方在將來開發應用程式和/或建議應用程式,建議您實施OAuth伺服器如果您想遵循Web API開發的最佳實踐。

但是,混淆可能無法解決。當您想要讓用戶使用他們的外部服務帳戶(如Facebook和Twitter)登錄您的網站時。由於「OAuth身份驗證」這一術語經常在此上下文中使用,因此您可能認為必須為您的服務實施OAuth。但是,在這種情況下,由於您的服務是使用外部服務實施的OAuth的客戶端,因此您的服務本身不必實施OAuth。確切地說,您的服務必須編寫代碼以使用其他公司的OAuth。換句話說,從外部服務的角度來看,您的服務必須表現為OAuth客戶端。但是,在此用例中,您的服務不必像OAuth伺服器那樣運行。也就是說,您不必實現OAuth伺服器。

3.認證和授權

我解釋了讓人們感到困惑的術語 - 「OAuth身份驗證」。

每個解釋都說「OAuth是授權規範,而不是身份驗證規範。」這是因為RFC 6749(OAuth 2.0授權框架)明確指出認證「超出了本規範的範圍。」以下段落摘自「 3.1。 RFC 6749中的「授權端點」。

授權端點用於與資源所有者交互並獲得授權授權。授權伺服器必須首先驗證資源所有者的身份。授權伺服器驗證資源所有者的方式(例如,用戶名和密碼登錄,會話cookie)超出了本規範的範圍。


儘管如此,「OAuth身份驗證」一詞泛濫並使人們感到困惑。這種混淆不僅在商業方面,而且在工程師中也是如此。例如,「OAuth授權與身份驗證」之類的問題有時會發布到Stack Overflow(我對問題的回答是這個)。

由術語,認證和授權(在OAuth的上下文中)處理的信息可以描述如下。

  1. 身份驗證 - 誰是誰。
  2. 授權 - 誰授予誰誰的權限。

身份驗證是一個簡單的概念換句話說,它是對身份的確認。在網站上識別人的最流行方式是請求該人提供一對ID和密碼,但還有其他方式,如使用指紋或虹膜的生物識別身份驗證,一次性密碼,隨機數字表等。無論如何,無論使用何種方式,身份驗證都是識別身份的過程。使用開發人員的話,可以表示為「身份驗證是識別用戶唯一標識符的過程」。

另一方面,授權是複雜的,因為涉及三個元素,即「誰」,「什麼權限」和「對誰」。另外,令人困惑的是,在這三個要素中,識別「誰」是認證的過程。換句話說,授權過程包括認證過程作為一個部分的事實使事情變得混亂。

如果三個元素應該被開發人員使用的單詞替換,「who」可以替換為「user」,「who」替換為「client application」。因此,OAuth上下文中的授權可以說是用戶向客戶端應用程式授予權限的過程。

下圖描繪了到目前為止所解釋的概念。

此圖說明了授權頁面(用戶授予客戶端應用程式權限的頁面)中的哪些部分用於身份驗證和授權。身份驗證和授權之間的區別很明顯。

現在,是時候談論「OAuth身份驗證」了。

因為授權過程包括認證過程作為一部分,所以授權意味著認證。因此,有些人開始使用OAuth進行身份驗證。這是「OAuth身份驗證」,並且由於「管理用戶憑據的任務可以委託給外部服務」以及「新用戶開始使用該服務的障礙因為用戶而變得更低」等優點而迅速占據主導地位註冊過程可以省略。「

OpenID的人對這種情況抱有怨恨。 - 抱歉,我不知道他們是否真的有這種感覺,但至少我可以想像他們認為OAuth身份驗證遠遠超出他們之前定義的規範級別,如OpenID 2.0和SAML。然而,不可否認的是,他們的規範並沒有占上風,世界各地的開發人員都選擇了OAuth身份驗證的簡易性。因此,他們在OAuth之上定義了一個新的身份驗證規範OpenID Connect。 OpenID Connect常見問題解答將關係描述為如下所示的等式。


(Identity, Authentication) + OAuth 2.0 = OpenID Connect

由於這一點,OpenID Connect的身份驗證可以在OAuth授權過程中同時執行。

由於業界的主要參與者一直致力於規範創建和主動實施(FAQ),OpenID Connect肯定會占上風。因此,OmniAuth等OAuth身份驗證庫將逐漸完成其角色。

但是,人們肯定會變得更加困惑,因為用於身份驗證的OpenID Connect建立在用於授權的OAuth之上。很難解釋,特別是在我的情況下,因為Authlete專注於授權,雖然它支持OpenID Connect,但它不會對身份驗證做任何事情。在開始向客戶解釋產品本身之前,我總是要解釋身份驗證和授權之間的區別。

關於OAuth身份驗證的問題,請閱讀John Bradley先生的文章「OAuth for Authentication的問題」。在文章中他說:「這是一個安全漏洞,你可以開車穿過。」

「再說OAuth是一種認證標準。」Nat Sakimura先生和John Bradley先生。 (來自https://twitter.com/ve7jtb/status/740650395735871488)

4. OAuth 2.0和OpenID Connect之間的關係

然而,到目前為止,所有內容只是這篇文章的序言。開發人員的技術內容從這裡開始。第一個主題是OAuth 2.0和OpenID Connect之間的關係。

在我完成RFC 6749(OAuth 2.0授權框架)的實施之後,我注意到了OpenID Connect的存在。當我收集有關OpenID Connect的信息時,我認為我應該實現該功能,因此請閱讀OpenID Connect Core 1.0和其他相關規範。在閱讀之後,我得出的結論是「所有人都應該從頭開始重寫」。

OpenID Connect網站稱「OpenID Connect 1.0是一個基於OAuth 2.0協議的簡單身份層。」這給人的印象是OpenID Connect可以在現有的OAuth 2.0實現之上輕鬆無縫地實現。然而,事實卻完全不同。恕我直言,OpenID Connect實際上是OAuth 3.0。

有許多與OpenID Connect相關的規範,它們令人費解,難以破譯它們。在我能夠掌握整個畫面之前,我幾乎瘋了,不得不讀了三遍。與OpenID Connect規範相比,RFC 6749可以說很容易。


5.響應類型

特別是,與現有實現衝突的是處理請求參數response_type的方法。可以肯定的是,RFC 6749聲明請求參數可能需要多個值,但這是將來的可能性。如果我們直接讀取RFC 6749,則response_type是代碼或令牌。幾乎不可能想像這兩個是同時設置的。這是因為該參數用於確定處理來自客戶端應用程式的請求的流程。具體而言,當response_type的值是代碼時使用授權代碼流,並且當值是token時使用隱式流。誰能想像這些流量是混合的?即使可以想像它,我們應該如何解決流量之間存在的衝突?例如,授權代碼流要求將響應參數嵌入到重定向URI(4.1.2。授權響應)的查詢部分中,而隱式流要求將響應參數嵌入到片段部分中(4.2.2。訪問令牌)響應),並不能同時滿足這些要求。

但是,OpenID Connect已將id_token添加為response_type的新值,並明確允許將code,token和id_token的任意組合作為response_type的值。此外,也沒有添加。詳情見「3。身份驗證「OpenID Connect Core 1.0和OAuth 2.0多響應類型編碼實踐」。

它需要進行重大更改才能修改在假定選擇或選擇的情況下編寫的現有代碼,以便它可以處理可能值和混合流的任意組合。因此,如果將來有可能支持OpenID Connect,OAuth庫的實現者應該從頭開始用OpenID Connect編寫它。換句話說,現有的OAuth庫無法在不進行重大修改的情況下支持OpenID Connect。

例如,Spring Security OAuth。此庫尚未支持OpenID Connect(截至2016年6月)。對於要支持OpenID Connect的庫,首先,請求參數response_type必須能夠採用除代碼和令牌之外的其他值。對它的請求被列為「問題#619處理其他response_types」,但它尚未得到解決,並且該主題的最後一條評論是「任何評論都非常受歡迎,因為事實證明(正如我預測的那樣)a大型重構練習。「我閱讀了一些相關的源文件,發現支持OpenID Connect需要進行大的修改才是真的。除非一些公司在財務上支持該項目,否則我擔心該項目需要很長時間才能支持OpenID Connect。

順便說一句,我也想提到Apache Oltu。該項目聲稱它支持OpenID Connect,但我的猜測是初始實現僅支持OAuth 2.0,並且在稍後階段添加了OpenID Connect支持。我認為這樣的原因是OAuth 2.0(org.apache.oltu.oauth2)的包和OpenID Connect(org.apache.oltu.openidconnect)的包是隔離的。但是,這種方法會破壞架構。例如,OpenIdConnectResponse類是OAuthAccessTokenResponse的後代是不合適的,因為包含ID令牌的響應不一定包含訪問令牌。其他示例是存在名為OAuthClientRequest.AuthenticationRequestBuilder的類(由於某些原因而不是「授權」但是「身份驗證」)以及存在GitHub特定的類GitHubTokenResponse。 Apache Oltu的架構至少給我帶來了問題。我不知道有關該項目的細節,但在我個人看來,它註定要縮小。

6.客戶端應用程式的元數據

正如在RFC 6749的客戶端註冊中明確寫出的那樣,客戶端應用程式必須在發出授權請求之前提前註冊到目標授權伺服器。因此,在典型情況下,授權伺服器的實現者定義資料庫表以存儲關於客戶端應用程式的信息。

要確定表應該具有哪些列,實現者通過閱讀規範來列出項目。例如,閱讀RFC 6749將使您意識到至少需要以下項目。

  1. Client ID
  2. Client Secret
  3. Client Type
  4. Redirect URIs

除此之外,實現者可以添加更多屬性。例如,「應用程式名稱」。

即使您通過RFC 6749進行搜索,客戶端應用程式的屬性也沒有那麼多,因此存儲客戶端應用程式屬性的資料庫表的列數不會變大 - 這樣的好日子已經因為出現了OpenID Connect。客戶端應用程式應具有的許多屬性列在2. OpenID Connect動態客戶端註冊1.0的客戶端元數據中。以下是清單。

  1. redirect_uris - 客戶端使用的重定向URI值。
  2. response_types - 客戶端聲明它將自己限制為使用的response_type值。
  3. grant_types - 授權客戶端聲明它將限制自己使用的類型。
  4. application_type - 應用程式的種類。
  5. 聯繫人 - 負責此客戶的人員的電子郵件地址。
  6. client_name - 要呈現給最終用戶的客戶端的名稱。
  7. logo_uri - 引用客戶端應用程式徽標的URL。
  8. client_uri - 客戶端主頁的URL。
  9. policy_uri-依賴方客戶端向最終用戶提供的URL,以了解如何使用配置文件數據。
  10. tos_uri-依賴方客戶提供給最終用戶的URL,以了解依賴方的服務條款。
  11. jwks_uri-客戶端的JSON Web密鑰集文檔的URL。
  12. jwks - 客戶端的JSON Web Key Set文檔,按值傳遞。
  13. sector_identifier_uri - 使用https方案的URL,用於由OP計算偽名標識符。
  14. subject_type - 要求對此客戶的響應的subject_type。
  15. id_token_signed_response_alg - 簽署發給此客戶端的ID令牌所需的JWS alg算法。
  16. id_token_encrypted_response_alg - 加密發給此客戶端的ID令牌所需的JWE alg算法。

  17. id_token_encrypted_response_enc-加密發給該客戶端的ID令牌所需的JWE enc算法。
  18. userinfo_signed_response_alg-簽署UserInfo響應所需的JWS alg算法。
  19. userinfo_encrypted_response_alg - 加密UserInfo響應所需的JWE alg算法。
  20. userinfo_encrypted_response_enc - 加密UserInfo響應所需的JWE enc算法。
  21. request_object_signing_response_alg - 必須用於簽署發送給OP的請求對象的JWS alg算法。
  22. request_object_encryption_alg - RP聲明它可以用於加密發送給OP的請求對象的JWE alg算法。
  23. request_object_encryption_enc - JWE enc算法,RP聲明它可以用於加密發送給OP的請求對象。
  24. token_endpoint_auth_method-請求端點的請求客戶端身份驗證方法。
  25. token_endpoint_auth_signing_alg - 必須用於對JWT進行簽名的JWS alg算法,該JWT用於在令牌端點對private_key_jwt和client_secret_jwt身份驗證方法的客戶端進行身份驗證。
  26. default_max_age - 默認最大認證年齡。
  27. require_auth_time - 布爾值,指定是否需要ID令牌中的auth_time聲明。
  28. default_acr_values - 默認請求的身份驗證上下文類參考值。
  29. initiate_login_uri - 使用https方案的URI,第三方可以使用該方案來啟動RP的登錄。
  30. request_uris - 由RP預先註冊以在OP上使用的request_uri值。

因此,客戶端應用程式的資料庫表應該能夠存儲這些信息。此外,應該注意的是,允許本地化某些屬性(例如client_name,tos_uri,policy_uri,logo_uri和client_uri)(2.1。元數據語言和腳本)。需要額外考慮資料庫表設計來存儲本地化屬性值。

以下小節是我對客戶應用程式屬性的個人意見。

6.1 客戶類型

我擔心定義規範是一種錯誤2. OpenID Connect動態客戶端註冊1.0的客戶端元數據不包含「客戶端類型」。我認為這樣做的原因是,當我們實現授權伺服器時,必須考慮兩種客戶端類型之間的區別,「機密」和「公共」(在2.1。客戶端類型的RFC 6749中定義)。事實上,「客戶端類型」被列為要在2.註冊RFC 6749的客戶端註冊的客戶端屬性的示例如下。

...註冊可以依賴於其他方式來建立信任並獲得所需的客戶端屬性(例如,重定向URI,客戶端類型)。

如果這不是錯誤,則必須就動態客戶端註冊註冊的客戶端應用程式的客戶端類型達成共識。但是,我無法在相關規範中找到此類信息。

無論如何,我認為在為客戶端應用程式定義資料庫表時,應該存在客戶端類型的列。

您可以在問題991中找到關於此的一些討論。

6.2。申請類型

根據規範,application_type是可選屬性。 application_type的預定義值是native和web。如果省略,則將web用作默認值。

如果省略時使用默認值,則自然結果是客戶端應用程式的應用程式類型必須是本機和Web。因此,您可能希望在application_type的列中添加NOT NULL。但是,Authlete的實現不敢添加NOT NULL並允許NULL。

原因是我不確定應用於每個OAuth 2.0客戶端的OpenID Connect動態客戶端註冊1.0中定義的application_type所施加的重定向URI值的限制。

使用OAuth隱式授權類型的Web客戶端必須僅使用https方案註冊URL作為redirect_uris;他們不能使用localhost作為主機名。本機客戶端必須僅使用自定義URI方案或URL使用http:scheme註冊redirect_uris,並使用localhost作為主機名。

2年前,我發布了一個問題「應用程式類型(OpenID Connect)是否與客戶端類型(OAuth 2.0)對應?」到Stack Overflow,但我無法得到任何答案。所以我自己調查和回答。如果有興趣請看。

6.3。客戶秘密

客戶秘密的長度應該是多長時間?

例如,「OpenAM管理指南」使用密碼作為客戶端機密值的示例。下面是12.4.1的截圖。將OpenAM配置為授權伺服器和客戶端。

似乎OpenAM允許用戶使用短字符串作為客戶端密鑰。

另一方面,在Authlete的實現中,客戶端機密自動生成並變得像下面那樣長。

GBAyfVL7YWtP6gudLIjbRZV_N0dW4f3xETiIxqtokEAZ6FAsBtgyIq0MpU1uQ7J08xOTO2zwP0OuO3pMVAUTid

這個長度的原因是我想支持512位的對稱簽名和加密算法。例如,我想支持HS512作為JWS的簽名算法。因為客戶機密碼必須具有512位或更多的熵以支持HS512,所以上述示例的長度是86,這是使用base64url編碼512位數據的結果。

關於對稱簽名和加密算法的熵,OpenID Connect Core 1.0中的16.19對稱密鑰熵如下所述。

在10.1節和10.2節中,密鑰是從client_secret值派生的。因此,當與對稱簽名或加密操作一起使用時,client_secret值必須包含足夠的熵以生成加密強密鑰。此外,client_secret值還必須至少包含所使用的特定算法的MAC密鑰所需的最小八位位元組數。因此,例如,對於HS256,client_secret值必須包含至少32個八位位元組(並且幾乎可以肯定應該包含更多,因為client_secret值可能使用受限制的字母表)。

並且,3.1。 RFC 7518(JSON Web算法)中的JWS的「alg」(算法)頭部參數值指出必須支持HS256(使用SHA-256的HMAC)作為JWS的簽名算法。作為合乎邏輯的結果,任何聲稱符合OpenID Connect的實現都需要生成具有256位或更多熵的客戶機密鑰。

6.4。簽名算法

id_token_signed_response_alg列在「2。 「OpenID Connect動態客戶端註冊1.0的客戶端元數據」。它表示客戶端應用程式要求授權伺服器用作ID令牌的簽名算法的算法。如上所述,有效值列在RFC 7518中,應注意不允許任何值。如果在註冊時省略id_token_signed_response_alg的值,則使用RS256。

userinfo_signed_response_alg也是客戶端應用程式要求授權伺服器使用的簽名算法。該算法用於簽署從UserI返回的信息

這是偏離主題的,但是為nv-websocket-client(日語信息)創建了一個問題,這是一個用於Java的WebSocket客戶端庫我在GitHub上向公眾開放。問題是一個功能改進的提議,表明當開發人員同時調用setSSLContext()方法和setSSLSocketFactory()方法時,庫有一個警告機制。之所以提出這個提案,是因為記者對這兩種方法的不正當行為感到不安。我的回答是它在JavaDoc中明確寫出了當調用這兩個方法時哪個設置優先,並且這樣的插入檢查會使WebSocketFactory類難以使用。然後,反應是「在調用這兩種方法之前,先沒有詳細閱讀文檔,這是我的錯。但是,您認為有多少其他開發人員會在犯同樣錯誤之前先詳細閱讀文檔?「

哦,如果開發人員由於他/她沒有閱讀文件的原因而浪費時間在自製錯誤上,這只是一個當之無愧的懲罰......

幫助那些不閱讀文件的人的試驗將是無止境的。即使庫阻止了alg = none的簽名,這些工程師也會毫不猶豫地將私鑰包含在通過授權伺服器的JWK Set端點發布的JWK集中。為什麼?你認為那些不讀文件的人可以注意到JWKSet類的toPublicJWKSet()方法的存在(在Nimbus JOSE + JWT庫中)並理解方法的含義嗎?可能,他們天真地說,「是的,我可以創建一個JWKSet類的實例。我們發布吧!我已經完成了JWK Set端點的實現!「

不參考RFC等主要來源的工程師無法發現他們找到的答案中的錯誤,並毫無疑問地相信答案。但是,工程師必須避免閱讀RFC以成為真正的工程師。

要成為真正的工程師,請不要避免閱讀RFC。只搜索技術博客和Stack Overflow尋找答案永遠不會把你帶到正確的地方。

6.5。 Client Application Developer

一些開源授權伺服器提供了一種機制,可以動態註冊客戶端應用程式,如HTML表單(ForgeRock的OpenAM)和Web API(MITRE的MITREid Connect)。但是,似乎只有授權伺服器的管理員才能註冊客戶端應用程式。但是,理想的方法是創建類似於Twitter的應用程式管理控制台,讓開發人員登錄,並提供一個環境,讓每個開發人員都可以註冊和管理他/她自己的客戶端應用程式。為此,客戶端應用程式的資料庫表應該有一個包含開發人員唯一標識符的列。

它經常被遺忘,因為實現授權伺服器本身很麻煩,但是還需要提供管理客戶端應用程式的機制,以便向公眾開放Web API。如果Web API的預期用戶僅限於封閉組,則授權伺服器的管理員可以在每次請求他/她時註冊客戶端應用程式。事實上,有一家公司的管理員為每個註冊請求手動鍵入SQL語句。但是,如果要向公眾開放Web API,此類操作將無法運行,您將意識到必須為客戶端應用程式提供合適的管理控制台。如果您成功確保了開發授權伺服器和Web API的預算,但忘記了為客戶端應用程式確保管理控制台的預算,則會導致「已實現Web API但無法向公眾開放」。

作為此類管理控制台的示例,Authlete為上述用例提供了開發者控制台。 Authlete本身不管理開發人員帳戶,但通過名為「開發人員身份驗證回調」的機制,其帳戶由Authlete客戶管理的開發人員可以使用開發人員控制台。因此,Authlete客戶不必為客戶端應用程式開發管理控制台。


7.訪問令牌

7.1。訪問令牌表示

如何表示訪問令牌?有兩種主要方式。

作為無意義的隨機字符串。與訪問令牌相關聯的信息存儲在授權伺服器後面的資料庫表中。

作為一個自包含的字符串,它是通過base64url或類似的東西對訪問令牌信息進行編碼的結果。

在這些方式之間進行選擇將導致後續差異,如下表所述。

如果訪問令牌是隨機字符串,則每次都需要查詢授權伺服器以獲取有關訪問令牌的信息。相反,如果訪問令牌本身包含信息,則無需查詢授權伺服器。這使得自包含樣式聽起來更好,但是因為必須對授權伺服器進行查詢以檢查訪問令牌是否已被撤銷,即使採用自包含樣式,在任何情況下,網絡通信也是如此。每次客戶端應用程式呈現訪問令牌時都需要。

自包含樣式中的繁瑣之處在於,每次請求訪問令牌撤銷時,我們必須添加表示「已撤銷」的記錄,並且必須保留此類記錄,直到訪問令牌到期為止。否則,如果刪除了記錄,則撤銷的訪問令牌將被復活並再次生效(如果尚未達到原始到期日期)。

相反,在隨機字符串樣式的情況下,可以簡單地通過刪除訪問令牌記錄本身來實現訪問令牌撤銷。因此,由於任何意外,撤銷訪問令牌無法復活。此外,不會發生在獨立風格中觀察到的負面影響「撤銷增加記錄」。

要啟用訪問令牌吊銷,即使在自包含樣式的情況下,也必須為訪問令牌分配唯一標識符。否則,無法分辨哪個訪問令牌已被撤銷。換句話說,授權伺服器採用自包含樣式但不為訪問令牌分配唯一標識符是授權伺服器,它不能撤銷訪問令牌。它可能是實現策略之一,但是這樣的授權伺服器不應該發出長期訪問令牌,也不應該發出刷新令牌。

「無法撤銷訪問令牌的授權伺服器?!」,您可能想知道。但是,這種授權確實存在。某個全球大型系統集成商收購了一家公司,並正在使用被收購公司的產品開發授權伺服器,但在後期階段,系統集成商及其客戶注意到授權伺服器無法撤銷訪問令牌。當我聽到這個故事時,我猜想授權伺服器會發出沒有唯一標識符的自包含樣式的訪問令牌。

自包含的樣式看起來很好,因為有一些優點,例如「無需查詢授權伺服器來提取訪問令牌的信息」和「無需在授權伺服器端維護訪問令牌記錄」,但是當你考慮訪問令牌撤銷,有討論的餘地。

7.2。訪問令牌刪除

為防止資料庫無限增長,應定期從資料庫中刪除過期的訪問令牌。

請求授權伺服器不必要地發出訪問令牌的客戶端應用程式是麻煩製造者。雖然他們已經有一個尚未過期的訪問令牌,但他們會重複丟棄這樣一個有效的訪問令牌並請求新的令牌。如果發生這種情況,則會在資料庫中累積未使用但無法刪除的訪問令牌(因為它們尚未過期)。

要防止出現這種情況,請將訪問令牌最後一次使用的時間戳保存到資料庫中,以及訪問令牌到期的時間戳,並定期運行程序,以便長時間刪除未使用的訪問令牌。當然,它取決於服務的特性是否可以在未過期時刪除未使用的訪問令牌。

在此之前,我遇到了一位工程師,他在某個大公司的OAuth實施項目中工作,而他卻屬於該公司。他告訴我,系統的構建沒有考慮訪問令牌的刪除,因此系統的資料庫可能擁有數以億計的訪問令牌。嚇人,可怕。當開發生成某個東西的系統時,應該同時考慮刪除生成的東西的時間。

8.重定向URI

8.1。重定向URI驗證

2014年5月,獲博士學位。新加坡的學生髮表了一篇文章,它引起了人們對「OAuth中的漏洞?」的討論,這是一個關於所謂的Covert Redirect的問題。那些正確理解OAuth 2.0的人很快意識到這不是由於規範中的漏洞而是由於不正確的實現。然而,該主題讓很多人感到不安,OAuth領域的專家無法幫助編寫解釋性文檔。約翰布拉德利先生的「隱蔽重定向及其對OAuth和OpenID Connect的真正影響」就是其中一個文件。

如果未正確處理重定向URI,則會出現安全問題。相關規範中描述了如何處理重定向URI,但很難正確實現它,因為有許多事情要關注,例如,(a)RFC 6749的要求和OpenID Connect的要求是不同的(b) )必須考慮客戶端應用程式的application_type屬性的值。

如何正確處理重定向URI的部分取決於實現者如何仔細和詳盡地閱讀相關規範。因此,讀取部件的實現代碼可以很好地猜測整個授權伺服器的實現質量。所以,每個人都盡最大努力實施它!

......如果我冷冷地拋棄了你,到目前為止我讀過我的長篇文章,我會感到很遺憾,所以我向你展示了Authlete的實施訣竅。以下是處理授權請求中包含的redirect_uri參數的偽代碼。請注意,偽代碼不必分解為可瀏覽性的方法,但在實際的Authlete實現中,代碼流很好地分解為方法。因此,出於性能目的,實際代碼流與偽代碼不同。 (如果實際的實現包含如此多的嵌套if和for偽像,那將是一種恥辱。)

// Extract the value of the 'redirect_uri' parameter from
// the authorization request.
redirectUri = ...
// Remember whether a redirect URI was explicitly given.
// It must be checked later in the implementation of the
// token endpoint because RFC 6749 states as follows.
//
// redirect_uri
// REQUIRED, if the "redirect_uri" parameter was
// included in the authorization request as described
// in Section 4.1.1, and their values MUST be identical.
//
explicit = (redirectUri != null);
// Extract registered redirect URIs from the database.
registeredRedirectUris = ...
// Requirements by RFC 6749 (OAuth 2.0) and those by
// OpenID Connect are different. Therefore, the code flow
// branches according to whether the request is an OpenID
// Connect request or not. This is judged by whether the
// 'scope' request parameter contains 'openid' as a value.
if ( 'openid' is included in 'scope' )
{
// Check requirements by OpenID Connect.
// If the 'redirect_uri' is not contained in the request.
if ( redirectUri == null )
{
// The 'redirect_uri' parameter is mandatory in
// OpenID Connect. It's optional in RFC 6749.
throw new Exception(
"The 'redirect_uri' parameter is missing.");
}
// For each registered redirect URI.
for ( registeredRedirectUri : registeredRedirectUris )
{
// 'Simple String Comparison' is required by the
// specification.
if ( registeredRedirectUri.equals( redirectUri ) )
{
// OK. The redirect URI specified by the
// authorization request is registered.
registered = true;
break;
}
}
// If the redirect URI specified by the authorization
// request matches none of the registered redirect URIs.
if ( registered == false )
{
throw new Exception(
"The redirect URI is not registered.");
}
}
else
{
// Check requirements by RFC 6749.
// If redirect URIs are not registered at all.
if ( registeredRedirectUris.size() == 0 )
{
// RFC 6749, 3.1.2.2. Registration Requirements says
// as follows:
//
// The authorization server MUST require the
// following clients to register their
// redirection endpoint:
//
// o Public clients.
// o Confidential clients utilizing the
// implicit grant type.
// If the type of the client application which made
// the authorization request is 'public'.
if ( client.getClientType() == PUBLIC )
{
throw new Exception(
"A redirect URI must be registered.");
}
// If the client type is 'confidential' and if the
// authorization flow is 'Implicit Flow'. If the
// 'response_type' request parameter contains either
// or both of 'token' and 'id_token', the flow should
// be treated as a kind of 'Implicit Flow'.
else if ( responseType.requiresImplicitFlow() )
{
throw new Exception(
"A redirect URI must be registered.");
}
}
// If the authorization request does not contain the
// 'redirect_uri' request parameter.
if ( redirectUri == null )
{
// If redirect URIs are not registered at all,
// or if multiple redirect URIs are registered.
if ( registeredRedirectUris.size() != 1 )
{
// A redirect URI must be explicitly specified
// by the 'redirect_uri' parameter.
throw new Exception(
"The 'redirect_uri' parameter is missing.");
}
// One redirect URI is registered. Use it as the
// default value of redirect URI.
redirectUri = registeredRedirectUris[0];
}
// The authorization request contains the 'redirect_uri'
// parameter, but redirect URIs are not registered.
else if ( registeredRedirectUris.size() == 0 )
{
// The code flow reaches here if and only if the
// client type is 'confidential' and the authorization
// flow is not 'Implicit Flow'. In this case, the
// redirect URI specified by the 'redirect_uri'
// parameter of the authorization request is used
// although it is not registered. However,
// requirements written in RFC 6749, 3.1.2.
// Redirection Endpoint are checked.
// If the specified redirect URI is not an absolute one.
if ( redirectUri.isAbsolute() == false )
{
throw new Exception(
"The 'redirect_uri' is not an absolute URI.");
}
// If the specified redirect URI has a fragment part.
if ( redirectUri.getFragment() != null )
{
throw new Exception(
"The 'redirect_uri' has a fragment part.");
}
}
else
{
// If the specified redirect URI is not an absolute one.
if ( redirectUri.isAbsolute() == false )
{
throw new Exception(
"The 'redirect_uri' is not an absolute URI.");
}
// If the specified redirect URI has a fragment part.
if ( redirectUri.getFragment() != null )
{
throw new Exception(
"The 'redirect_uri' has a fragment part.");
}
// For each registered redirect URI.
for (registeredRedirectUri : registeredRedirectUris )
{
// If the registered redirect URI is a full URI.
if ( registeredRedirectUri.getQuery() != null )
{
// 'Simple String Comparison'
if ( registeredRedirectUri.equals( redirectUri ) )
{
// The specified redirect URI is registered.
registered = true;
break;
}
// This registered redirect URI does not match.
continue;
}
// Compare the scheme parts.
if ( registeredRedirectUri.getScheme().equals(
redirectUri.getScheme() ) == false )
{
// This registered redirect URI does not match.
continue;
}
// Compare the user information parts. Here I use
// an imaginary method 'equalsSafely()' because
// the code would become too long if I inlined it.
// The method compares arguments without throwing
// any exception even if either or both of the
// arguments are null.
if ( equalsSafely(
registeredRedirectUri.getUserInfo(),
redirectUri.getUserInfo() ) == false )
{
// This registered redirect URI does not match.
continue;
}
// Compare the host parts. Ignore case sensitivity.
if ( registeredRedirectUri.getHost().equalsIgnoreCase(
redirectUri.getHost() ) == false )
{
// This registered redirect URI does not match.
continue;
}
// Compare the port parts. Here I use an imaginary
// method 'getPortOrDefaultPort()' because the
// code would become too long if I inlined it. The
// method returns the default port number of the
// scheme when 'getPort()' returns -1. The last
// resort is 'URI.toURL().getDefaultPort()'. -1 is
// returned If 'getDefaultPort()' throws an exception.
if ( getPortOrDefaultPort( registeredRedirectUri ) !=
getPortOrDefaultPort( redirectUri ) )
{
// This registered redirect URI does not match.
continue;
}
// Compare the path parts. Here I use the imaginary
// method 'equalsSafely()' again.
if ( equalsSafely( registeredRedirectUri.getPath(),
redirectUri.getPath() ) == false )
{
// This registered redirect URI does not match.
continue;
}
// The specified redirect URI is registered.
registered = true;
break;
}
// If none of the registered redirect URI matches.
if ( registered == false )
{
throw new Exception(
"The redirect URI is not registered.");
}
}
}
// Check requirements by the 'application_type' of the client.// If the value of the 'application_type' attribute is 'web'.
if ( client.getApplicationType() == WEB )
{
// If the authorization flow is 'Implicit Flow'. When the
// 'response_type' request parameter of the authorization
// request contains either or both of 'token' and 'id_token',
// it should be treated as a kind of 'Implicit Flow'.
if ( responseType.requiresImplicitFlow() )
{
// If the scheme of the redirect URI is not 'https'.
if ( "https".equals( redirectUri.getScheme() ) == false )
{
// The scheme part of the redirect URI must be
// 'https' when a client application whose
// 'application_type' is 'web' uses 'Implicit Flow'.
throw new Exception(
"The scheme of the redirect URI is not 'https'.");
}
// If the host of the redirect URI is 'localhost'.
if ( "localhost".equals( redirectUri.getHost() ) )
{
// The host of the redirect URI must not be
// 'localhost' when a client application whose
// 'application_type' is 'web' uses 'Implicit Flow'.
throw new Exception(
"The host of the redirect URI is 'localhost'.");
}
}
}
// If the value of the 'application_type' attribute is 'native'.
else if ( client.getApplicationType() == NATIVE )
{
// If the scheme of the redirect URI is 'https'.
if ( "https".equals( redirectUri.getScheme() ) )
{
// The scheme of the redirect URI must not be 'https'
// when the 'application_type' of the client is 'native'.
throw new Exception(
"The scheme of the redirect URI is 'https'.");
}
// If the scheme of the redirect URI is 'http'.
if ( "http".equals( redirectUri.getScheme() ) )
{
// If the host of the redirect URI is not 'localhost'.
if ( "localhost".equals(
redirectUri.getHost() ) == false )
{
// When a client application whose 'application_type'
// is 'native' uses a redirect URI whose scheme is
// 'http', the host port of the URI must be
// 'localhost'.
throw new Exception(
"The host of the redirect URI is not 'localhost'.");
}
}
}
// If the value of the 'application_type' attribute is neither
// 'web' or 'native'.
else
{
// As mentioned above, Authlete allows 'unspecified' as a
// value of the 'application_type' attribute. Therefore,
// no exception is thrown here.
}

8.2。其他的實施

在OpenID Connect中,redirect_uri參數是必需的,關於如何檢查呈現的重定向URI是否已註冊的要求只是「簡單字符串比較」。因此,如果您需要關注的只是OpenID Connect,那麼實現將非常簡單。例如,在2016年10月在GitHub上贏得大約1,700顆星並且已通過OpenID認證計劃認證的IdentityServer3中,檢查重定向URI的實現如下(摘自DefaultRedirectUriValidator.cs以及其他格式化新行)。

public virtual Task IsRedirectUriValidAsync(

string requestedUri, Client client)

{

return Task.FromResult(

StringCollectionContainsString(

client.RedirectUris, requestedUri));

}

OpenID Connect只關心手段,換句話說,授權伺服器不接受傳統授權代碼流和範圍請求參數中不包含openid的隱式流。也就是說,這樣的授權伺服器無法響應任何現有的OAuth 2.0客戶端應用程式。

那麼,IdentityServer3是否拒絕傳統的授權請求?看看AuthorizeRequestValidator.cs,你會發現這個(格式化已經調整):

if (request.RequestedScopes.Contains(

Constants.StandardScopes.OpenId))

{

request.IsOpenIdRequest = true;

}

//////////////////////////////////////////////////////////

// check scope vs response_type plausability

//////////////////////////////////////////////////////////

var requirement =

Constants.ResponseTypeToScopeRequirement[request.ResponseType];

if (requirement == Constants.ScopeRequirement.Identity

requirement == Constants.ScopeRequirement.IdentityOnly)

{

if (request.IsOpenIdRequest == false)

{

LogError("response_type requires the openid scope", request);

return Invalid(request, ErrorTypes.Client);

}

}

您無需了解此代碼的詳細信息。關鍵是有一些路徑允許在scope參數中不包含openid的情況。也就是說,接受傳統的授權請求。如果是這樣,IdentityServer3的實現是不正確的。但是,另一方面,在AuthorizeRequestValidator.cs中的另一個位置,實現拒絕所有不包含redirect_uri參數的授權請求,如下所示(格式化已調整)。

//////////////////////////////////////////////////////////

// redirect_uri must be present, and a valid uri

//////////////////////////////////////////////////////////

var redirectUri = request.Raw.Get(Constants.AuthorizeRequest.RedirectUri);

if (redirectUri.IsMissingOrTooLong(

_options.InputLengthRestrictions.RedirectUri))

{

LogError("redirect_uri is missing or too long", request);

return Invalid(request);

}

因此,實現不必關心省略redirect_uri參數的情況。但是,因為redirect_uri參數在RFC 6749中是可選的,所以行為 - 沒有redirect_uri參數的授權請求被無條件拒絕,儘管傳統的授權請求被接受 - 違反了規範。此外,IdentityServer3不會對application_type屬性進行驗證。要實現驗證,作為第一步,必須將application_type屬性的屬性添加到表示客戶端應用程式(Client.cs)的模型類中,因為當前實現錯過了它。

9.違反規範

細微違反規範的行為有時被稱為「方言」。 「方言」一詞可能給人一種「可接受」的印象,但違法行為是違法行為。如果沒有方言,則為每種計算機語言提供一個通用OAuth 2.0 / OpenID Connect庫就足夠了。但是,在現實世界中,違反規範的授權伺服器需要自定義客戶端庫。

Facebook的OAuth流程需要其自定義客戶端庫的原因是Facebook的OAuth實現中存在許多違反規範的行為。例如,(1)逗號用作範圍列表的分隔符(它應該是空格),(2)來自令牌端點的響應的格式是application / x-www-form-urlencoded(它應該是JSON) ,以及(3)訪問令牌的到期日期參數的名稱是過期的(應該是expires_in)。

Facebook和其他大牌公司不僅違反了規範。以下是其他示例。

9.1。範圍清單的分隔符

範圍名稱列在授權端點和令牌端點的請求的範圍參數中。 RFC 6749,3.3。訪問令牌範圍要求將空格用作分隔符,但以下OAuth實現使用逗號:

  • Facebook
  • GitHub
  • Spotify
  • Discus
  • Todoist

9.2 令牌端點的響應格式

RFC 6749,5.1。成功響應要求來自令牌端點的成功響應的格式為JSON,但以下OAuth實現使用application / x-www-form-urlencoded:

  • Facebook
  • Bitly
  • GitHub

默認格式為application / x-www-form-urlencoded,但GitHub提供了一種請求JSON的方法。

9.3 來自令牌端點的響應中的token_type

RFC 6749,5.1。成功響應要求token_type參數包含在來自令牌端點的成功響應中,但以下OAuth實現不包含它:

鬆弛

Salesforce也遇到過這個問題(OAuth訪問令牌響應丟失token_type),但它已被修復。

9.4 token_type不一致

以下OAuth實現聲稱令牌類型為「Bearer」,但其資源端點不接受通過RFC 6750(OAuth 2.0授權框架:承載令牌使用)中定義的方式訪問令牌:

GitHub(它通過授權格式接受訪問令牌:令牌OAUTH-TOKEN)

9.5 grant_type不是必需的

grant_type參數在令牌端點是必需的,但以下OAuth實現不需要它:

  • GitHub
  • Slack
  • Todoist

9.6 錯誤參數的非官方值

規範已為錯誤參數定義了一些值,這些值包含在授權伺服器的錯誤響應中,但以下OAuth實現定義了自己的值:

GitHub(例如application_suspended)

Todoist(例如bad_authorization_code)

9.7。錯誤時參數名稱錯誤

以下OAuth實現在返回錯誤代碼時使用errorCode而不是error:


10.代碼交換的證明密鑰

10.1。 PKCE是必須的

你知道PKCE嗎?它是一個定義為RFC 7636(OAuth公共客戶端的代碼交換證明密鑰)的規範,於2015年9月發布。它是針對授權代碼攔截攻擊的對策。

攻擊成功需要一些條件,但如果您考慮發布智慧型手機應用程式,強烈建議客戶端應用程式和授權伺服器都支持PKCE。否則,惡意應用程式可能攔截授權伺服器發出的授權代碼,並將其與授權伺服器的令牌端點處的有效訪問令牌交換。

在2012年10月發布了RFC 6749(OAuth 2.0授權框架),因此即使熟悉OAuth 2.0的開發人員也可能不知道2015年9月最近發布的RFC 7636。但是,應該注意的是「OAuth 2.0 for Native Apps」草案表明,在某些情況下,它的支持是必須的。

客戶端和授權伺服器都必須支持PKCE [RFC7636]使用自定義URI方案或環回IP重定向。授權伺服器應該使用自定義方案拒絕授權請求,或者如果不存在所需的PKCE參數,則將環回IP作為重定向URI的一部分,返回PKCE [RFC7636]第4.4.1節中定義的錯誤消息。建議將PKCE [RFC7636]用於應用程式聲明的HTTPS重定向URI,即使這些URI通常不會被攔截,以防止對應用程式間通信的攻擊。

支持RFC 7636的授權伺服器的授權端點接受兩個請求參數:code_challenge和code_challenge_method,令牌端點接受code_verifier。並且在令牌端點的實現中,授權伺服器使用(a)客戶端應用程式呈現的代碼驗證器和(b)客戶端應用程式在授權端點處指定的代碼質詢方法來計算代碼質詢的值。如果計算的代碼質詢和客戶端應用程式在授權端點處呈現的code_challenge參數的值相等,則可以說發出授權請求的實體和發出令牌請求的實體是相同的。因此,授權伺服器可以避免向惡意應用程式發出訪問令牌,該惡意應用程式與發出授權請求的實體不同。

RFC 7636的整個流程在Authlete的網站上進行了說明:代碼交換的證明密鑰(RFC 7636)。如果您有興趣,請閱讀。

10.2 伺服器端實現

在授權端點的實現中,授權伺服器必須做的是將授權請求中包含的code_challenge參數和code_challenge_method參數的值保存到資料庫中。因此,實現代碼中沒有任何有趣的內容。需要注意的是,想要支持PKCE的授權伺服器必須將code_challenge和code_challenge_method的列添加到存儲授權碼的資料庫表中。

Authlete的完整原始碼是保密的,但是為了您的興趣,我在這裡向您展示了實際的Authlete實現,它驗證了令牌端點處code_verifier參數的值。

private void validatePKCE(AuthorizationCodeEntity acEntity)

{

// See RFC 7636 (Proof Key for Code Exchange) for details.

// Get the value of 'code_challenge' which was contained in

// the authorization request.

String challenge = acEntity.getCodeChallenge();

if (challenge == null)

{

// The authorization request did not contain

// 'code_challenge'.

return;

}

// If the authorization request contained 'code_challenge',

// the token request must contain 'code_verifier'. Extract

// the value of 'code_verifier' from the token request.

String verifier = extractFromParameters(

"code_verifier", invalid_grant, A050312, A050313, A050314);

// Compute the challenge using the verifier

String computedChallenge = computeChallenge(acEntity, verifier);

if (challenge.equals(computedChallenge))

{

// OK. The presented code_verifier is valid.

return;

}

// The code challenge value computed with 'code_verifier'

// is different from 'code_challenge' contained in the

// authorization request.

throw toException(invalid_grant, A050315);

}

private String computeChallenge(

AuthorizationCodeEntity acEntity, String verifier)

{

CodeChallengeMethod method = acEntity.getCodeChallengeMethod();

// This should not happen, but just in case.

if (method == null)

{

// Use 'plain' as the default value required by RFC 7636.

method = CodeChallengeMethod.PLAIN;

}

switch (method)

{

case PLAIN:

// code_verifier

return verifier;

case S256:

// BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

return computeChallengeS256(verifier);

default:

// The value of code_challenge_method extracted

// from the database is not supported.

throw toException(server_error, A050102);

}

}

private String computeChallengeS256(String verifier)

{

// BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

// SHA256

byte[] hash =

Digest.getInstanceSHA256().update(verifier).digest();

// BASE64URL

return SecurityUtils.encode(hash);

}

用於實現computeChallengeS256(String)方法的Digest類包含在我的開源庫nv-digest中。它是一個實用程序庫,可以輕鬆進行摘要計算。使用此庫,計算SHA-256摘要值可以寫成一行,如下所示。

byte[] hash = Digest.getInstanceSHA256().update(verifier).digest();

10.3。客戶端實施

客戶端應用程式必須為PKCE做些什麼。一種是生成一個由43-128個字母組成的隨機碼驗證器,使用代碼驗證器和代碼質詢方法(plain或S256)計算代碼質詢,並包括計算出的代碼質詢和代碼質詢方法作為值授權請求中的code_challenge參數和code_challenge_method參數。另一種是在令牌請求中包含代碼驗證器。

作為客戶端實現的示例,我將介紹以下兩個。

  1. AppAuth for Android
  2. AppAuth for iOS

它們是用於與OAuth 2.0和OpenID Connect伺服器通信的SDK。他們聲稱他們包括最佳實踐並支持PKCE。

如果為code_challenge_method = S256實現計算邏輯,則可以通過在代碼驗證器的值為dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk時檢查代碼質詢的值是否變為E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM來測試它。這些值在RFC 7636的「附錄B. S256 code_challenge_method的示例」中作為示例值找到。


11.最後

有些人可能會說實施OAuth和OpenID Connect很容易,其他人可能會說不是。在任何一種情況下,事實上,即使是擁有足夠預算和人力資源的Facebook和GitHub等大型科技公司也未能正確實施OAuth和OpenID Connect。著名的開源項目如Apache Oltu和Spring Security也存在問題。因此,如果您自己實施OAuth和OpenID Connect,請認真對待並準備一個體面的開發團隊。否則,安全風險將會增加。

僅僅實現RFC 6749並不困難,但是從頭開始實施OpenID Connect會讓您發瘋。因此,建議使用現有實現作為起點。第一步是在OpenID Connect網站中搜索與OAuth和OpenID Connect相關的軟體的「庫,產品和工具」頁面(儘管未列出Authlete)。當然,作為Authlete,Inc。的聯合創始人,如果您選擇Authlete,我將很高興。

感謝您閱讀這篇長篇文章。

原文:https://medium.com/@darutk/full-scratch-implementor-of-oauth-and-openid-connect-talks-about-findings-55015f36d1c3

本文:http://pub.intelligentx.net/node/510

討論:請加入知識星球或者小紅圈【首席架構師圈】

文章來源: https://twgreatdaily.com/zh-my/0kb99GwBJleJMoPMM9wh.html