天天看點

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

可能需要看一點點預備知識

OAuth 2.0 不完全簡介: https://www.cnblogs.com/cgzl/p/9221488.html

OpenID Connect 不完全簡介: https://www.cnblogs.com/cgzl/p/9231219.html

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證
Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證
Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證
Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

實際上OpenID Connect 是完全相容OAuth 2.0的. 

本系列文章主要關注OpenID Connect的三個流程

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證
Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

本文隻介紹Hybrid Flow. 而根據其response_type的不同, 它又分為三種情況:

response_type=code id_token

response_type=code token

response_type=code id_token token

注意:為了表明是OpenID Connect協定的請求, scope參數裡必須包含openid.

當reponse_type為這種類型的時候, 授權碼和ID Token從授權端點發行傳回, 然後Access Token 和 ID Token會從Token端點發行傳回:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

當reponse_type為這種類型的時候, 授權碼和Access Token從授權端點發行傳回, 然後Access Token 和 ID Token會從Token端點發行傳回:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

當reponse_type為這種類型的時候, 授權碼和Access Token和ID Token從授權端點發行傳回, 然後Access Token 和 ID Token會從Token端點發行傳回:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

Identity Server 4 是OpenID Connect和OAuth 2.0的架構, 它主要是為ASP.NET Core準備的. 它得到了OpenID基金會的官方認證. 它也是開源的, https://github.com/IdentityServer/IdentityServer4.

首先需要一個現成的API項目, 其實本文根本沒用到: https://github.com/solenovex/Identity-Server-4-Tutorial-Code, 在該連接配接的00目錄裡. 

在此之上, 我再繼續搭建Identity Server 4.

在該解決方案裡建立一個ASP.NET Core Web Application:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

由于Identity Provider 通常不是為某一個用戶端項目或API資源所準備的, 是以該項目的名稱通常獨立于其它項目的名稱. 在這裡我教它Dave.IdentityProvider.

然後選擇Empty模闆, 并使用ASP.NET Core 2.1:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

點選OK, 項目建立好之後, 為該項目安裝Identity Server 4, 我通過Nuget:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

随後是配置Identity Server 4.

打開Dave.IdentityProvider的Startup.cs, 在ConfigureServices裡面調用 services.AddIdentityServer()來把Identity Server注冊到ASP.NET Core的容器裡面; 随後我調用了services.AddDeveloperSigningCredentials()方法, 它會建立一個用于對token簽名的臨時密鑰材料(但是在生産環境中應該使用可持久的密鑰材料):

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

然後需要添加資源和用戶端, 按照官方文檔的做法, 我添加一個Config類:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

這裡我首先添加了一個GetUsers()方法, 裡面有兩個最終使用者.

注意TestUser的SubjectId屬性的值, 在這個Identity Provider裡面必須是唯一的.

每個使用者下面還有個Claims屬性, claims裡面都是代表使用者的一些資訊.

但是如何讓這些claims通過Identity Token傳回來呢?

Claims 與 Scope 是緊密相連的, 是多對一的. 下面我建立一個方法來傳回Scope:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

在這裡IdentityResource映射于那些關于使用者資訊的scope, 後邊還要介紹ApiResource, 它映射于API資源的scopes. IdentityResource就是一些關于使用者身份的資料, 例如user ID, name, email等等. 每個Identity Resource都有一個唯一的名稱, 你可以為它賦一些claims, 然後這些claims就會包含在該使用者的Identity Token裡面(這隻是一種情況), 用戶端需要使用scope參數來請求通路某個identity resource.

OpenID Connect協定裡的scopes可以了解為一組預定義的claims的簡稱. 

OpenID Connect預定義了幾組标準的scopes 或者叫 identity resources:

openid, 這個是必須的. 它會為使用者提供一個唯一的ID, 也叫做subject id. 它的出現也就是告訴授權伺服器用戶端發出的是OpenID Connect 請求. 它同時也要求傳回ID Token. 如果 response_type 含有 "token" (指的是Access Token), 那麼ID Token在授權的響應裡和Access Token一同傳回; 如果response_type 包含 "code" (指授權碼), 那麼ID Token會作為Token端點響應的一部分傳回.

profile, 這個是可選的. 這個scope請求通路的是最終使用者的個人資料, 但是不包括email, address和phone, 它包括的claims有: name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, 和 updated_at.

email, 這個是可選的. 它包括 email 和 email_verified 兩個claims.

address, 這個是可選的. 它隻有address 一個claim.

phone, 這個是可選的. 它包括 phone_number 和 phone_number_verified 兩個claims.

其中通過profile, email, address, phone這四個scope請求的claims, 如果請求的response_type的值包含"token"(指的是access token), 那麼這些claims是從使用者資訊端點(UserInfo Endpoint)傳回的. 而如果response_type不包含Access Token, 那麼這些claims是在ID Token裡面傳回.

Identity Server 4的IdentityResources類裡面包含着上述這5個預定義的scopes.

是以上面方法裡TestUser的given_name和family_name将會在ID_Token裡面傳回.

最後, 還需要定義用戶端:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

暫時它還隻是傳回一個空的集合.

這個Config類先到這, 現在還需要再修改一下Startup裡的ConfigureServices方法, 把上面Config裡面的配置都加進去:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

然後修改Startup裡的Configure方法, 把IdentityServer添加到ASP.NET Core的管道裡:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證
Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

我直接修改的launchSettings.json檔案, 隻保留了這一部分.

然後運作程式, 通路該網址: https://localhost:5001/.well-known/openid-configuration, 會得到以下畫面就說明Identity Server 4配置成功了:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

Identity Server 4 的UI可以在這裡找到: https://github.com/IdentityServer/IdentityServer4.Quickstart.UI

根據文檔描述, 在Dave.IdentityProvider項目目錄下打開Powershell執行這句話即可安裝UI:

安裝好之後可以看到項目檔案的變化:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

但是由于這套UI使用了ASP.NET Core MVC, 是以我還需要再配置一些東西.

在Startup的ConfigureServices裡, 注冊MVC:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

在Startup的Configure裡, 在管道裡使用靜态檔案和MVC:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

再次運作程式, 首頁如下:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

點選discovery document, 它就是我之前打開的那個頁面.

首先考慮ASP.NET Core MVC 作為用戶端應用的情況.

ASP.NET Core MVC是機密用戶端(Confidential Client), 它是傳統的伺服器端Web應用.

它需要長時間通路(long-lived access), 是以需要refresh token. 那麼它可以使用Authorization Code Flow或Hybrid Flow.

在這裡Hybrid Flow是相對進階一些的, 它可以讓用戶端首先從授權端點獲得一個ID Token并通過浏覽器(front-channel)傳遞過來, 這樣我們就可以驗證這個ID Token. 如果驗證成功然後, 用戶端再打開一個後端通道(back-channel), 從Token端點擷取Access Token.

下面是OpenID Connect官方文檔給出的一個身份認證請求的例子.

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

第一行的URI: "/authorize" 就是授權端點(Authorization Endpoint), 它位于身份提供商(Identity provider, IDP)那裡. 這個URI可以從前面介紹的discovery document裡面找到.

第二行 response_type=code id_token, 它決定了采取了哪一種Hybrid流程(參考上面那三個圖).

第三行 client_id=xxxx, 這是用戶端的身份辨別.

第四行 redirect_uri=https...., 這是用戶端那裡的重定向端點(Redirection Endpoint).

第五行 scope=openid profile email, 這就是用戶端所請求的scopes.

再看一遍這張圖:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

為什麼要傳回兩次ID Token呢? 這是因為第(4)步裡面請求Token的時候要求用戶端身份認證, 這時請求Token的時候需要提供Authorization Code, Client ID和 Client Secret, 這些secret并不暴露給外界, 這些東西是由用戶端伺服器通過後端通道傳遞給Token端點的. 而第一次獲得的ID Token是從前端通道(浏覽器)傳回的. 

當這個ID Token被驗證通過之後, 也就證明了目前使用者到底是誰.

下面簡單對比一下前端和後端通道:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證
Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

建立好後回到IdentityProvider項目, 添加一個Client:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

這裡ClientName是用戶端名稱, 它會出現在使用者同意授權的頁面. 流程選擇的是Hybrid. 這裡暫時隻請求OpenId這一個Scope, 以便隻傳回ID Token, 在GetIdentityResources()方法裡我知道支援這個scope. 這個流程的授權碼和tokens是通過跳轉來傳遞到浏覽器的URI上面的, 是以我需要一個URI來接收這些東西, 而RedirectUris裡面的URI就是允許做這個工作的URI.

下面繼續配置MVC用戶端 (官方文檔: https://identityserver4.readthedocs.io/en/release/quickstarts/3_interactive_login.html#creating-an-mvc-client).

在MVC用戶端的Startup的ConfigureServices裡:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

下面的文字都是翻譯的官方文檔.

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 這句話是指, 我們關閉了JWT的Claim 類型映射, 以便允許well-known claims.

這樣做, 就保證它不會修改任何從Authorization Server傳回的Claims.

這裡通過調用services.AddAuthentication()方法來添加和配置身份認證中間件.

這裡我們使用Cookie作為驗證使用者的首選方式, 而DefaultScheme = "Cookies", 這個"Cookies"字元串是可以任意填寫的, 隻要與後邊的一緻即可. 但是如果同一個伺服器上有很多應用的話, 這個Scheme的名字不能重複.

而把DefaultChanllangeScheme設為"oidc", 這個名字與後邊配置OpenIdConnect的名字要一樣. 當使用者需要登陸的時候, 将使用的是OpenId Connect Scheme.

然後的AddCookie, 其參數是之前配置的DefaultScheme名稱, 這配置了Cookie的處理者, 并讓應用程式為我們的DefaultScheme啟用了基于Cookie的身份認證. 一旦ID Token驗證成功并且轉化為Claims身份辨別後, 這些資訊就将會儲存于被加密的Cookie裡.

下面的AddOpenIdConnect()方法添加了對OpenID Connect流程的支援, 它讓配置了用來執行OpenId Connect 協定的處理者.

這個處理者會負責建立身份認證請求, Token請求和其它請求, 并負責ID Token的驗證工作.

它的身份認證scheme就是之前配置的"oidc", 它的意思就是如果該用戶端的某部分要求身份認證的時候, OpenID Connect将會作為預設方案被觸發(因為之前設定的DefaultChallengeScheme是"oidc", 和這裡的名字一樣).

SignInScheme和上面的DefaultScheme一緻, 它保證身份認證成功的結果将會被儲存在方案名為"Cookies"的Cookie裡. 

Authority就是Identity Provider的位址.

ClientId和Secret要與IdentityProvider裡面的值一樣.

ResponseType就是前面介紹過的.

請求的Scope有openid和profile, 其實中間件預設也包括了這些scope, 但是寫出來更明确一些.

SaveTokens=true, 表示允許存儲從Identity Provider那裡獲得的tokens.

然後配置管道:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

確定中間件在UseMvc()之前調用.

還要確定監聽位址和IdentityProvider裡面配置的Client一緻:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

然後我對HomeController要求身份認證:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

随後修改一下About方法, 我僅僅是想展示token的資料:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

這個token來自于cookie.

再修改About的頁面:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

下面測試一下MVC用戶端的身份認證:

同時運作Identity Provider 和 Mvc 兩個程式, 最好使用控制台, 這樣如果有錯誤的話就可以友善的看到相關資訊了.

在通路Mvc的首頁時, 會自動跳轉到Identity Provider上:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

具體的請求可以通過Chrome的Developer Tools看到:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

在Identity Provider的控制台上, 也可以看到相關資訊:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

登入使用者之後, 就會看到征求使用者同意授權的頁面:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

點選Yes即可.

然後浏覽器會調轉會MVC Client, 通過Chrome的工具檢視:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

可以看到跳轉回來的時候是到了signin-oidc這個位址, 它就是我之前在Identity Provider裡面Client的RedirectUri.

與此同時, 可以在Identity Provider的控制台看到, MVC用戶端通過後端通道向Token端點發出了Token請求, 這個過程使用者是不會發現的:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

這個過程就和前面圖示的一樣, 最後從token端點請求到新的ID Token之後, 會再次進行驗證, 然後會通過它建立Claims Identity, 也就是前面代碼裡的User.Claims.

這個身份驗證的憑據都會儲存在加密的Cookie裡面:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

來到About菜單:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

最上面可以看到ID Token的值.

sid是sessionid.

sub是使用者的subjectid

idp是本地的.

我們可以在jwt.io來解析一下這個ID Token

解碼之後的ID Token:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

這裡的内容以後再講.

登入好用之後, 就考慮一下登出.

再_Layout.cshtml裡面添加登出按鈕, 這部分官方文檔都有:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

然後建立Action方法:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

首先要清除本地的Cookie, 這個Cookie的名字要與之前配置的預設方案裡的名字一緻, 這一步就相當于登出MVC用戶端.

後一行代碼的作用是跳轉回到Identity Provider, 然後使用者可以繼續登出IDP, 也就是IDP會清除它的Cookie.

但是登出之後, 使用者會留在Identity Provider那裡:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

檢視IDP的控制台, 可以看到這個失敗: Invalida post logout URI:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

這是因為我們配置Client的時候沒有指定在登出之後的跳轉URI位址.

回到IDP的用戶端配置那裡:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

添加PostLogoutRedirectUris屬性, 裡面這個值是就是預設的登出後跳轉位址.

再次操作後, 效果如下:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

點選here之後會回到MVC用戶端, 然後由于權限問題會又立即跳轉到IDP.

如果想讓這個過程自動跳轉, 可以修改IDP的Quickstart/Account/AccountOptions類裡面的這個值改成true:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

再次操作, 跳轉就是自動完成的了.

檢視解碼的ID Token, 可以看到裡面包含了這些claims:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

這裡除了sub之外, 并沒有關于使用者的其他資訊.

我們可以通過指定參數來要求在ID Token裡面傳回使用者其他的claims, 但是由于id token是從URI進行傳輸的, 而浏覽器會有URI的長度限制, 是以盡量讓token小點, 以免超限.

為了獲得使用者其他的claims, 用戶端應用可以使用使用者資訊端點, 這需要用access token和相關claims對應的scopes.

首先在MVC用戶端配置, GetClaimsFromUserInfoEndpoit為true, 并請求profile scope:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

随後在IDP那裡為MVC Client添加上profile scope:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

再次執行操作, 回到About頁面:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

可以看到profile scope裡對應的這兩個claims值已經出來了.

再把ID Token到jwt.io去解碼一下:

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證

可以看到這兩個claims并不在ID Token裡面, 這就說明它們來自使用者資訊端點.

在ID Token裡面的東西(官方文檔有介紹: http://openid.net/specs/openid-connect-core-1_0.html#IDToken):

sub是使用者的subjectid, 也就是使用者的身份辨別.

iss是ID Token的發行者.

aud是這個token的目标觀衆, 這裡就是MVC用戶端的clientid.

nbf是指在這個時間之前, ID Token是不被接受的.

exp是ID Token的過期時間.

iat是這個JWT token發行的時間.

auth_time是原始身份認證的時間.

amr是指身份認證的方法. 這裡用的是pwd, 密碼.

nonce, 它是Number only to be used once的意思. 它是一個字元串, 使用ID Token和用戶端Session關聯, 來減少重複攻擊.

最後是at_hash, 其實還有c_hash, 它們分别代表Access Token Hash和Code Hash. 就是通過某種方式對Access Token和Code的Base64編碼. 它們可以用來把Access Token或Authorization Code連結到這個ID Token上.

今天先到這.

代碼在: https://github.com/solenovex/Identity-Server-4-Tutorial-Code 的01部分.

Identity Server 4 - Hybrid Flow - MVC用戶端身份驗證