可能須要看一點點預備知識html
OAuth 2.0 不徹底簡介: http://www.javashuo.com/article/p-gtjytgin-ch.html前端
OpenID Connect 不徹底簡介: http://www.javashuo.com/article/p-dgnpoqsb-dc.htmlgit
實際上OpenID Connect 是徹底兼容OAuth 2.0的. github
本系列文章主要關注OpenID Connect的三個流程web
本文只介紹Hybrid Flow. 而根據其response_type的不一樣, 它又分爲三種狀況:shell
注意:爲了代表是OpenID Connect協議的請求, scope參數裏必須包含openid.json
當reponse_type爲這種類型的時候, 受權碼和ID Token從受權端點發行返回, 而後Access Token 和 ID Token會從Token端點發行返回:後端
當reponse_type爲這種類型的時候, 受權碼和Access Token從受權端點發行返回, 而後Access Token 和 ID Token會從Token端點發行返回:瀏覽器
當reponse_type爲這種類型的時候, 受權碼和Access Token和ID Token從受權端點發行返回, 而後Access Token 和 ID Token會從Token端點發行返回:服務器
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 Provider 一般不是爲某一個客戶端項目或API資源所準備的, 因此該項目的名稱一般獨立於其它項目的名稱. 在這裏我教它Dave.IdentityProvider.
而後選擇Empty模板, 並使用ASP.NET Core 2.1:
點擊OK, 項目創建好以後, 爲該項目安裝Identity Server 4, 我經過Nuget:
隨後是配置Identity Server 4.
打開Dave.IdentityProvider的Startup.cs, 在ConfigureServices裏面調用 services.AddIdentityServer()來把Identity Server註冊到ASP.NET Core的容器裏面; 隨後我調用了services.AddDeveloperSigningCredentials()方法, 它會建立一個用於對token簽名的臨時密鑰材料(可是在生產環境中應該使用可持久的密鑰材料):
而後須要添加資源和客戶端, 按照官方文檔的作法, 我添加一個Config類:
這裏我首先添加了一個GetUsers()方法, 裏面有兩個最終用戶.
注意TestUser的SubjectId屬性的值, 在這個Identity Provider裏面必須是惟一的.
每一個用戶下面還有個Claims屬性, claims裏面都是表明用戶的一些信息.
可是如何讓這些claims經過Identity Token返回來呢?
Claims 與 Scope 是緊密相連的, 是多對一的. 下面我創建一個方法來返回Scope:
在這裏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:
其中經過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裏面返回.
最後, 還須要定義客戶端:
暫時它還只是返回一個空的集合.
這個Config類先到這, 如今還須要再修改一下Startup裏的ConfigureServices方法, 把上面Config裏面的配置都加進去:
而後修改Startup裏的Configure方法, 把IdentityServer添加到ASP.NET Core的管道里:
我直接修改的launchSettings.json文件, 只保留了這一部分.
而後運行程序, 訪問該網址: https://localhost:5001/.well-known/openid-configuration, 會獲得如下畫面就說明Identity Server 4配置成功了:
Identity Server 4 的UI能夠在這裏找到: https://github.com/IdentityServer/IdentityServer4.Quickstart.UI
根據文檔描述, 在Dave.IdentityProvider項目目錄下打開Powershell執行這句話便可安裝UI:
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/release/get.ps1'))
安裝好以後能夠看到項目文件的變化:
可是因爲這套UI使用了ASP.NET Core MVC, 因此我還須要再配置一些東西.
在Startup的ConfigureServices裏, 註冊MVC:
在Startup的Configure裏, 在管道里使用靜態文件和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官方文檔給出的一個身份認證請求的例子.
第一行的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.
再看一遍這張圖:
爲何要返回兩次ID Token呢? 這是由於第(4)步裏面請求Token的時候要求客戶端身份認證, 這時請求Token的時候須要提供Authorization Code, Client ID和 Client Secret, 這些secret並不暴露給外界, 這些東西是由客戶端服務器經過後端通道傳遞給Token端點的. 而第一次得到的ID Token是從前端通道(瀏覽器)返回的.
當這個ID Token被驗證經過以後, 也就證實了當前用戶究竟是誰.
下面簡單對比一下前端和後端通道:
建立好後回到IdentityProvider項目, 添加一個Client:
這裏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裏:
下面的文字都是翻譯的官方文檔.
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.
而後配置管道:
確保中間件在UseMvc()以前調用.
還要確保監聽地址和IdentityProvider裏面配置的Client一致:
而後我對HomeController要求身份認證:
隨後修改一下About方法, 我僅僅是想展現token的數據:
這個token來自於cookie.
再修改About的頁面:
下面測試一下MVC客戶端的身份認證:
同時運行Identity Provider 和 Mvc 兩個程序, 最好使用控制檯, 這樣若是有錯誤的話就能夠方便的看到相關信息了.
在訪問Mvc的首頁時, 會自動跳轉到Identity Provider上:
具體的請求能夠經過Chrome的Developer Tools看到:
在Identity Provider的控制檯上, 也能夠看到相關信息:
登陸用戶以後, 就會看到徵求用戶贊成受權的頁面:
點擊Yes便可.
而後瀏覽器會調轉會MVC Client, 經過Chrome的工具查看:
能夠看到跳轉回來的時候是到了signin-oidc這個地址, 它就是我以前在Identity Provider裏面Client的RedirectUri.
與此同時, 能夠在Identity Provider的控制檯看到, MVC客戶端經過後端通道向Token端點發出了Token請求, 這個過程用戶是不會發現的:
這個過程就和前面圖示的同樣, 最後從token端點請求到新的ID Token以後, 會再次進行驗證, 而後會經過它建立Claims Identity, 也就是前面代碼裏的User.Claims.
這個身份驗證的憑據都會保存在加密的Cookie裏面:
來到About菜單:
最上面能夠看到ID Token的值.
sid是sessionid.
sub是用戶的subjectid
idp是本地的.
咱們能夠在jwt.io來解析一下這個ID Token
解碼以後的ID Token:
這裏的內容之後再講.
登陸好用以後, 就考慮一下登出.
再_Layout.cshtml裏面添加登出按鈕, 這部分官方文檔都有:
而後創建Action方法:
首先要清除本地的Cookie, 這個Cookie的名字要與以前配置的默認方案裏的名字一致, 這一步就至關於登出MVC客戶端.
後一行代碼的做用是跳轉回到Identity Provider, 而後用戶能夠繼續登出IDP, 也就是IDP會清除它的Cookie.
可是登出以後, 用戶會留在Identity Provider那裏:
查看IDP的控制檯, 能夠看到這個失敗: Invalida post logout URI:
這是由於咱們配置Client的時候沒有指定在登出以後的跳轉URI地址.
回到IDP的客戶端配置那裏:
添加PostLogoutRedirectUris屬性, 裏面這個值是就是默認的登出後跳轉地址.
再次操做後, 效果以下:
點擊here以後會回到MVC客戶端, 而後因爲權限問題會又當即跳轉到IDP.
若是想讓這個過程自動跳轉, 能夠修改IDP的Quickstart/Account/AccountOptions類裏面的這個值改爲true:
再次操做, 跳轉就是自動完成的了.
查看解碼的ID Token, 能夠看到裏面包含了這些claims:
這裏除了sub以外, 並無關於用戶的其餘信息.
咱們能夠經過指定參數來要求在ID Token裏面返回用戶其餘的claims, 可是因爲id token是從URI進行傳輸的, 而瀏覽器會有URI的長度限制, 因此儘可能讓token小點, 以避免超限.
爲了得到用戶其餘的claims, 客戶端應用可使用用戶信息端點, 這須要用access token和相關claims對應的scopes.
首先在MVC客戶端配置, GetClaimsFromUserInfoEndpoit爲true, 並請求profile scope:
隨後在IDP那裏爲MVC Client添加上profile scope:
再次執行操做, 回到About頁面:
能夠看到profile scope裏對應的這兩個claims值已經出來了.
再把ID Token到jwt.io去解碼一下:
能夠看到這兩個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部分.