我們在《ASP.NET Core項目實戰的課程》第一章里面給identity server4做了一個全面的介紹和示例的練習 ,這篇文章是根據大家對OIDC遇到的一些常見問題整理得出。
本文將涉及到以下幾個話題:
-
什么是OpenId Connect (OIDC)
-
OIDC 對oAuth進行了哪些擴展?
-
Identity Server4提供的OIDC認證服務(服務端)
-
ASP.NET Core的權限體系中的OIDC認證框架(客戶端)
什么是 OIDC
在了解OIDC之前,我們先看一個很常見的場景。假使我們現在有一個網站要集成微信或者新浪微博的登錄,兩者現在依然采用的是oAuth 2.0的協議來實現 。 關於微信和新浪微博的登錄大家可以去看看它們的開發文檔。
在我們的網站集成微博或者新浪微博的過程大致是分為五步:
-
准備工作:在微信/新浪微博開發平台注冊一個應用,得到AppId和AppSecret
-
發起 oAauth2.0 中的 Authorization Code流程請求Code
-
根據Code再請求AccessToken(通常在我們應用的后端完成,用戶不可見)
-
根據 AccessToken 訪問微信/新浪微博的某一個API,來獲取用戶的信息
-
后置工作:根據用戶信息來判斷是否之前登錄過?如果沒有則創建一個用戶並將這個用戶作為當前用戶登錄(我們自己應用的登錄邏輯,比如生成jwt),如果有了則用之前的用戶登錄。
中間第2到3的步驟為標准的oAuth2 授權碼模式的流程,如果不理解的可以參考阮一峰所寫的《理解oAuth2.0 》一文。我們主要來看第4和5步,對於第三方應用要集成微博登錄這個場景來說最重要的是我希望能快速拿到用戶的一些基本信息(免去用戶再次輸入的麻煩)然后根據這些信息來生成一個我自己的用戶跟微博的用戶Id綁定(為的是下次你使用微博登錄的時候我還能把你再找出來)。
oAuth在這里麻煩的地方是我還需要再請求一次API去獲取用戶數據,注意這個API和登錄流程是不相干的,其實是屬於微博開放平台叢多API中的一個,包括微信開放平台也是這樣來實現。這里有個問題是前面的 2和3是oAuth2的標准化流程,而第4步卻不是,但是大家都這么干(它是一個大家都默許的標准)
於是大家干脆就建立了一套標准協議並進行了一些優化,它叫OIDC
OIDC 建立在oAuth2.0協議之上,允許客戶端(Clients)通過一個授權服務(Authorization Server)來完成對用戶認證的過程,並且可以得到用戶的一些基本信息包含在JWT中。
OIDC對oAuth進行了哪些擴展?
在oAuth2.0授權碼模式的幫助下,我們拿到了用戶信息。
以上沒有認證的過程,只是給我們的應用授權訪問一個API的權限,我們通過這個API去獲取當前用戶的信息,這些都是通過oAuth2的授權碼模式完成的。 我們來看看oAuth2 授權碼模式的流程:
第一步,我們向authorize endpoint請求code的時候所傳遞的response_type表示授權類型,原來只有固定值code
GET /connect/authorize?response_type=code&client_id=postman&state=xyz&scope=api1 &redirect_uri=http://localhost:5001/oauth2/callback
第二步,上面的請求執行完成之后會返回301跳轉至我們傳過去的redirect_uri並帶上code
https://localhost:5001/oauth2/callback?code=835d584d4bc96d46ce49e27ebdbf272e40234d5f31097f63163f17da61fcd01c &scope=api1 &state=111271607
第三步,用code換取access token
POST /connect/token?grant_type=authorization_code&code=835d584d4bc96d46ce49e27ebdbf272e40234d5f31097f63163f17da61fcd01c &redirect_uri=http://localhost:5001/oauth2/callback &client_id=postman &client_secret=secret
通過這個POST我們就可以得到access_token
{ "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjV", "expires_in": 3600, "token_type": "Bearer" }
我們拿到access_token之后,再把access_token放到authorization頭請求 api來獲取用戶的信息。在這里,這個api不是屬於授權服務器提供的,而是屬於資源服務器。
OIDC給oAuth2進行擴展之后就填補了這個空白,讓我們可以授權它添加了以下兩個內容:
- response_type 添加IdToken
- 添加userinfo endpoint,用idToken可以獲取用戶信息
OIDC對它進行了擴展,現在你有三個選擇:code, id_token和 token,現在我們可以這樣組合來使用。
"response_type" value | Flow |
---|---|
code | Authorization Code Flow |
id_token | Implicit Flow |
id_token token | Implicit Flow |
code id_token | Hybrid Flow |
code token | Hybrid Flow |
code id_token token | Hybrid Flow |
我們簡單的來理解一下這三種模式:
-
Authorization Code Flow授權碼模式:保留oAuth2下的授權模式不變response_type=code
-
Implicit Flow 隱式模式:在oAuth2下也有這個模式,主要用於客戶端直接可以向授權服務器獲取token,跳過中間獲取code用code換accesstoken的這一步。在OIDC下,responsetype=token idtoken,也就是可以同時返回access_token和id_token。
-
Hybrid Flow 混合模式: 比較有典型的地方是從authorize endpoint 獲取 code idtoken,這個時候id_token可以當成認證。而可以繼續用code獲取access_token去做授權,比隱式模式更安全。
再來詳細看一下這三種模式的差異:
Property | Authorization Code Flow | Implicit Flow | Hybrid Flow |
---|---|---|---|
access token和id token都通過Authorization endpoint返回 | no | yes | no |
兩個token都通過token end point 返回 | yes | no | no |
用戶使用的端(瀏覽器或者手機)無法查看token | yes | no | no |
Client can be authenticated | yes | no | yes |
支持刷新token | yes | no | yes |
不需要后端參與 | no | yes | no |
我們來看一下通過Hybird如何獲取 code、id_token、_以及access_token,然后再用id_token向userinfo endpoint請求用戶信息。
第一步:獲取code,
- response_type=code id_token
- scope=api1 openid profile 其中openid即為用戶的唯一識別號
GET /connect/authorize?response_type=code id_token&client_id=postman&state=xyz&scope=api1 openid profile &nonce=7362CAEA-9CA5-4B43-9BA3-34D7C303EBA7 &redirect_uri=http://localhost:5001/oauth2/callback
當我們使用OIDC的時候,我們請求里面多了一個nonce的參數,與state有異曲同工之妙。我們給它一個guid值即可。
第二步:我們的redirect_uri在接收的時候即可以拿到code 和 id_token
https://localhost:5001/oauth2/callback# code=c5eaaaca8d4538f69f670a900d7a4fa1d1300b26ec67fba2f84129f0ab4ffa35 &id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjVjMzA5ZGIwYTE2OGEwOTgGtpbj0GVXNnkKhGdrzA &scope=openid%20profile%20api1&state=111271607
第三步:用code換access_token(這一步與oAuth2中的授權碼模式一致)
第四步:用access_token向userinfo endpoint獲取用戶資料
Get http://localhost:5000/connect/userinfo Authorization Bearer access_token
返回的用戶信息
{ "name": "scott", "family_name": "liu", "sub": "5BE86359-073C-434B-AD2D-A3932222DABE" }
以下是我們的流程示意圖。
有人可能會注意到,在這里我們拿到的idtoken沒有派上用場,我們的用戶資料還是通過access_token從userinfo endpoint里拿的。這里有兩個區別:
- userinfo endpoint是屬於認證服務器實現的,並非資源服務器,有歸屬的區別
- id_token 是一個jwt,里面帶有用戶的唯一標識,我們在判斷該用戶已經存在的時候不需要再請求userinfo endpoint
下圖是對id_token進行解析得到的信息:sub即subject_id(用戶唯一標識 )
對jwt了解的同學知道它里面本身就可以存儲用戶的信息,那么id_token可以嗎?答案當然是可以的,我們將在介紹完identity server4的集成之后最后來實現。
Identity Server4提供的OIDC認證服務
Identity Server4是asp.net core2.0實現的一套oAuth2 和OIDC框架,用它我們可以很快速的搭建一套自己的認證和授權服務。我們來看一下用它如何快速實現OIDC認證服務。
由於用戶登錄代碼過多,完整代碼可以加入ASP.NET Core QQ群 92436737獲取。 此處僅展示配置核心代碼。
過程
- 新建asp.net core web應用程序
- 添加identityserver4 nuget引用
- 依賴注入初始化
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetTestUsers());
- 中間件添加
app.UseIdentityServer();
- 配置
在測試的時候我們新建一個Config.cs來放一些配置信息
api resources
public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("api1", "API Application"){ UserClaims = { "role", JwtClaimTypes.Role } } }; }
identity resources
public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("api1", "API Application"){ UserClaims = { "role", JwtClaimTypes.Role } } }; }
clients
我們要講的關鍵信息在這里,client有一個AllowGrantTypes它是一個string的集合。我們要寫進去的值就是我們在上一節講三種模式: Code,Implict和Hybird。因為這三種模式決定了我們的response_type可以請求哪幾個值,所以這個地方一定不能寫錯。
IdentityServer4.Models.GrantTypes這個枚舉給我們提供了一些選項,實際上是把oAuth的4種和OIDC的3種進行了組保。
public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { ClientId = "postman", AllowedGrantTypes = GrantTypes.Hybird, RedirectUris = { "https://localhost:5001/oauth2/callback" }, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = new List<string> { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "api1" }, AllowOfflineAccess=true, }, }; }
users
public static List<TestUser> GetTestUsers() { return new List<TestUser> { new TestUser { SubjectId = "5BE86359-073C-434B-AD2D-A3932222DABE", Username = "scott", Password = "password", Claims = new List<Claim> { new Claim(JwtClaimTypes.Name, "scott"), new Claim(JwtClaimTypes.FamilyName, "liu"), new Claim(JwtClaimTypes.Email, "scott@scottbrady91.com"), new Claim(JwtClaimTypes.Role, "user"), } } }; }
ASP.NET Core的權限體系中的OIDC認證框架
在Microsoft.AspNetCore.All nuget引用中包含了Microsoft.AspNetCore.Authentication.OpenIdConnect即asp.net core OIDC的客戶端。我們需要在依賴注入中添加以下配置:
services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.SignInScheme = "Cookies"; options.Authority = "http://localhost:5000"; options.RequireHttpsMetadata = false; options.ClientId = "postman"; options.ClientSecret = "secret"; options.ResponseType = "code id_token"; options.GetClaimsFromUserInfoEndpoint = true; options.Scope.Add("api1"); options.Scope.Add("offline_access"); });
Authority即我們的用identity server4搭建的認證授權服務器,而其中的GetClaimsFromUserInfoEndpoint則會在拿到id_token之后自動向userinfo endpoint請求用戶信息並放到asp.net core的User Identity下。
我們上面講過,可以不需要請求userinfo endpoint, 直接將用戶信息放到id_token中。
這樣我們就不需要再向userinfo endpoint發起請求,從id_token中即可以獲取到用戶的信息。而有了identity server4的幫助,完成這一步只需要一句簡單的配置即可:
new Client { ClientId = "postman", AlwaysIncludeUserClaimsInIdToken = true, AllowOfflineAccess=true, }
這樣我們在拿到id_token之后,里即包含了我們的用戶信息。
資料:
曉晨master的identity server4中文文檔 http://www.cnblogs.com/stulzq/p/8119928.html
李念輝身份認證核心: https://www.cnblogs.com/linianhui/archive/2017/05/30/openid-connect-core.html
OIDC協議: http://openid.net/specs/openid-connect-discovery-1_0.html
Jesse騰飛的asp.net core項目實戰第一章identity server4准備 http://video.jessetalk.cn/course/5