驗證與授權


現代應用程式的組成複雜,驗證與授權的流程也隨之繁複,現代框架試圖將這一切隱藏起來,令開發者更難以區別驗證與授權的差異,實際上這是兩件不同的任務,既可以在單一容器中實作,在涉及第三方時,也有 OAuth2、OpenID Connect 等協定規範。

  • 區別驗證與授權

事情一開始很單純,「caterpillar 才能看到這個訊息」既然如此,應用程式如何驗證你真的是 caterpillar?驗證(Authentication)是套證明身份(identity)的機制,例如,例如 authenticate(name, passwd) 方法,定義如何使用 namepasswd 進行驗證,驗證方式不僅是基於名稱及密碼,也有可能基於憑證(Certificate)之類的機制。

一旦 caterpillar 通過驗證就可以看到訊息,也就是說另外有個機制決定了,訊息資源可否授權觀看,授權(Authorization)定義了身份與資源之間的存取控制規則,例如,if(authorized()) { show("message"); } 這個流程,定義了 "message" 是否可以顯示。

在最簡單的情境中,驗證與授權可能混雜在一起,例如「caterpillar 才能看到這個訊息」,只要 if(authenticate(name, passwd)) { show("message"); } 就可以實現,這時 authenticate() 與上述的 authorized() 任務就重疊了;然而,應用程式有一定的複雜性之後,驗證與授權的概念就會被分離開來。

例如在 Web 容器中,使用者驗證通過之後(authenticate() 的實作傳回 true),常見在 Session 物件中放個 Token 代表已驗證,後續資源頁面判斷 Session 中存在 Token(authorized() 傳回 true),就顯示相關的訊息資源,這時 authenticate()authorized() 就是被分離的驗證與授權機制。

在更複雜的情況下,就必須有更複雜的驗證、授權方式,像是 Java EE 容器安全或 Spring Security 之類的框架,就定義了使用者、角色、資源等名詞,在驗證成功後會以某方式儲存 Token,其中包含角色等資訊,而在存取資源時,憑藉的是角色與資源之間已定義好的對應關係,決定是否授予資源

  • OAuth2授權協定

當應用程式只運行在單一容器時,使用者與資源、驗證與授權只要在單一容器上定義或實作就可以了;然而,若應用程式的資源是分散在多台機器(多個容器),存取每個資源前,若都要進行驗證就是件麻煩事了,若客戶端能夠在驗證一次之後,帶著授權資訊來請求多個資源伺服器,資源伺服器不進行驗證,只認授權資訊來提供資源就好了。

最簡單的方式之一,就是在驗證通過後,客戶端透過 HTTP 基本驗證的原理,也就是透過 BASIC 標頭來攜帶授權資訊,然而這只適用於簡單的情境,在更複雜的場景中,需要將授權流程獨立出來,在驗證無誤之後,授權伺服器發給客戶端 Access Token,客戶端拿著 Access Token 請求多個資源擁有者,資源擁有者確認 Access Token 合法性之後,才授予受保護的資源。

為了讓這類被獨立出來的授權流程有一致性,就有了 OAuth 規範,在先前專欄〈從簡單到繁複的 OAuth2〉就談過,依需求的不同,目前 OAuth2 就規範了四種授權類型流程;不過,OAuth2 本身沒有規範 Access Token 應該是什麼樣子,為了增加 Access Token 的安全(像是避免被竄改),以及增加 Token 本身攜帶資訊的能力,可以使用 JSON Web Tokens,簡稱 JWT,它對 Token 制定了規範,具有對 Token 簽署,資源伺服器可以直接確認 Token 等優點。

必須區別的是,雖然 OAuth2 在流程中會涉及授權伺服器,授權伺服器在一開始勢必得處理驗證的問題,然而怎麼處理驗證,並不在 OAuth2 的規範之中,在 OAuth2 的官方網站一開始也寫了,它是個用於授權的協定(protocol for authorization)。

在 OAuth2 結合 JWT 的場景中,Token 中可能會帶有使用者名稱、角色等資訊,有些開發者誤以為這是用於驗證,實際上這些資訊是用於授權,Token 中的資訊是在驗證過後才能取得;另一方面,Token 只提供授權資訊,資源提供者收到 Token 後,如何運用其中資訊來決定資源的提供方式,OAuth2 也不規範這塊。

簡單來說,OAuth2 只是個協定,規範了如何請求授權資訊、提供授權資訊,然而沒有規範如何實作驗證、如何根據授權資訊提供資源等,這是 Java EE 或 Spring Security 等安全框架的事,OAuth2 與這類安全框架,基本上不存在取代的關係。

  • 驗證協定OpenID Connect

OAuth2 是第三方授權的協定規範,那是否有第三方驗證的規範呢?也就是將驗證相關資訊,註冊在可信任的身分提供者(Identity Provider),在依賴方(Relaying Party)需要驗證的場合時,由身分提供者來提供身份資訊,依賴方取得身份後進行驗證,以便進一步使用依賴方的功能,有些社交網站常被用來作為身份提供者,可直接使用社交網站上的帳號登入,因此這類機制常被稱為社交登入(Social login)。

曾經的 OpenID 1/2 是獨立的協定,也曾經被 Google、Yahoo 等支援,然而後來有些開發者,試著使用 OAuth2 結合 JWT,在 JWT 中放入驗證資訊以實現驗證(而不是授權),進一步地建立了 OpenID Connect(簡稱 OIDC)規範,由於 OIDC 是基於 OAuth2,在認識 OAuth2 的情況下,會比較容易理解 OpenID Connect。

OIDC 改變了 OAuth2 的部份語意以用於驗證,相對於 OAuth2 取得的 Access Token 是用於授權,OIDC 中 ID Token 是用來驗證使用者是否為其宣稱的身分,其中也包含其他使用者資訊,使用 JWT 來攜帶資訊,OIDC 也規範了取得 ID Token 後如何對其進行驗證。

簡單來說,在 OAuth2 中,授權伺服器在一開始得處理驗證的問題,然而怎麼處理驗證,並不在 OAuth2 的規範之中,OIDC 補足了這塊,例如,授權伺服器也許一開始用 Spring Security 透過資料庫進行驗證,現在可以實作 AuthenticationProvider,透過 OIDC 從第三方 OpenID 提供者取得 ID Token 進行驗證,不過要注意的是,在驗證通過之後,之後的授權流程等就不關 OIDC 的事了。

  • 試著從規範來理解

OAuth2 或 OpenID Connect 規範的細節很多,想瞭解並不容易,現代程式庫或框架隱藏了許多細節,只留下必要的部份給開發者設定或實作,更多時候是第三方應用程式隱藏了更多流程,只留下自己的一套設定給開發者遵守,JWT 其實只是個資訊載體,其中可能包含授權、驗證資訊或兩者皆有,這一切混淆在一起後,就常令開發者往往搞不清楚,現在是在做驗證還是授權。

若是如此,開發者應該試著從規範來理解整個流程,釐清哪些細節被程式庫、框架或應用程式隱藏了,如此一來,才能認清何時該用 Java EE 容器安全或 Spring Security,哪時該用 OAuth2,哪個地方又該採用 OpenID Connect。