在上上一篇基於OIDC的SSO的登錄頁面的截圖中有出現QQ登錄的地方。這個其實是通過擴展OIDC的OpenID Provider來實現的,OpenID Provider簡稱OP,OP是OIDC的一個很重要的角色,OIDC用它來實現兼容眾多的用戶認證方式的,比如基於OAuth2,SAML和WS-Federation等等的用戶認證方式。關於OP在[認證授權] 4.OIDC(OpenId Connect)身份認證授權(核心部分)(OIDC可以兼容眾多的IDP作為OIDC的OP來使用)中有提到過,但是並未詳細解釋。
由於QQ的開發者賬號申請不方便,故而在一下的示例中使用了Github的OAuth 2.0作為替代(原理是一模一樣的),源碼中已增加對Github OAuth 2.0 的支持。
由於dev頂級域名已被Google所持有並且強制Chrome對dev使用https(不便於查看http消息),故而改為了test頂級域名。
上一篇博客中的登錄時采用的本地的賬戶和密碼來運行的。本篇則為OIDC Server添加一個OP:Github OAuth 2.0。這就使得oidc-server.test可以使Github來登錄,並且SSO的客戶端可以不做任何改動(除非客戶端需要指定采用何種認證方式,即使如此也是非常非常微小的改動)。本篇涉及到的部分有(本系列的源代碼位於https://github.com/linianhui/oidc.example):
- oauth2.github.aspnetcore這個項目,它基於aspnetcore2實現了Github OAuth 2.0認證。
- oidc-server.test站點,對應的是web.oidc.server.ids4這個項目,引用了上面的這個項目。
- oidc-client-implicit.test站點,作為oidc的客戶端,Github登錄的最終消費者(它無需關注Github登錄的任何細節)。
1 OIDC-Client
1.1 指定oidc-server.test使用Github認證(可選)
下圖是上一篇中起始頁面,這次我們點擊Oidc Login(Github)這個鏈接(客戶端也可以不指定采用Github進行認證,推遲到進入oidc-server.test之后進行選擇)。
我們知道這個鏈接會返回一個302重定向,重定向的地址是發往oidc-server.test的認證請求,我們看下這個請求和上一次有什么差異:
除了紅色部分之外,其他地方並沒有任何的不同。那么我們就可以理解為時 acr_values=idp:github (其中idp是Identity Provider的縮寫,即身份提供商,和OP的OpenId Provider屬於一類含義,只是不同的叫法)這個參數改變了oidc-server.test的認證行為,使其選擇了Github進行登錄。
至此我們可以得出一個結論,那就是Github登錄無需在 oidc-server.test 的客戶端這邊進行處理,只需指定一個參數即可,比如如果oidc-server.test還支持了微信登錄,那么客戶端就可以通過傳遞acr_values=idp:wechat即可直接使用微信登錄。但是oidc-server.test內部是怎么實現的呢?這里有兩件事情需要處理:
- oidc-server.test要能夠識別oidc客戶端傳遞過來的這個參數,如果參數有效,則使用參數指定的OP進行登錄,如果沒有指定,則采用默認的登錄方式(本地的用戶和密碼體系)。參數是 acr_values(Authentication Context Class Reference values),它是oidc協議規定的一個參數,Ids4實現了對這個參數的支持。
- oidc-server.test需要支持使用Github進行登錄,並且關聯到ids4組件。
下面我們看看oidc-server.test這個站點是如何完成這兩件事情的。
2 OIDC-Server
2.1 識別客戶端發送的IDP信息
在oidc-server.test這個站點中,在集成ids4組件的時候,有這么一段代碼:
1 public static IServiceCollection AddIds4(this IServiceCollection @this) 2 { 3 @this 4 .AddAuthentication() 5 .AddQQConnect("qq", "QQ Connect", SetQQConnectOptions) 6 .AddGithub("github", "Github", SetGithubOptions); 7 8 @this 9 .AddIdentityServer(SetIdentityServerOptions) 10 .AddDeveloperSigningCredential() 11 .AddInMemoryIdentityResources(Resources.AllIdentityResources) 12 .AddInMemoryApiResources(Resources.AllApiResources) 13 .AddInMemoryClients(Clients.All) 14 .AddTestUsers(Users.All); 15 16 return @this; 17 } 18 19 20 private static void SetGithubOptions(GithubOAuthOptions options) 21 { 22 options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; 23 options.ClientId = GlobalConfig.Github.ClientId; 24 options.ClientSecret = GlobalConfig.Github.ClientSecret; 25 }
AddGithub 這個擴展方法是我自己寫的,位於文章開始提到的oauth2.github.aspnetcore項目中。我們暫且先不關注其內部是如何實現的,這里有兩個重要的信息。
- “github”,這是方法的第1個參數,指定了Github作為aspnetcore這個框架種支持的一種認證方式的唯一標識符,也就是一個scheme名字。
- options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; 其含義是把上面指定的github這個認證方式,作為ids4的外部登錄來使用。其實ExternalCookieAuthenticationScheme 也是個字符串而已 public const string ExternalCookieAuthenticationScheme = "idsrv.external"; ,這個字符串是ids4定義的一個外部登錄的sheme名字。所有的外部登錄如果想要和ids4集成,都需要使用它來關聯。
2.2 集成Github登錄
有了上述兩個信息,ids4就可以在接收到 acr_values=idp:github這樣的參數時,就可以自動的從aspnetcore框架中已經注冊的認證scheme中查找名為gtihub的認證方式,然后來觸Github登錄的流程。並且在Github認證完成后,進入ids4定義的外部登錄流程中。從Fiddler中可以看到這個重定向的過程:
然后Github就打開了它的登錄頁面:
這部分的控制代碼位於GithubOAuthHandler類繼承的OAuthHandler基類 BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) 方法中:
1 protected virtual string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) 2 { 3 var scopeParameter = properties.GetParameter<ICollection<string>>(OAuthChallengeProperties.ScopeKey); 4 var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope(); 5 6 var state = Options.StateDataFormat.Protect(properties); 7 var parameters = new Dictionary<string, string> 8 { 9 { "client_id", Options.ClientId }, 10 { "scope", scope }, 11 { "response_type", "code" }, 12 { "redirect_uri", redirectUri }, 13 { "state", state }, 14 }; 15 return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters); 16 }
BuildChallengeUrl 方法返回的URL地址,正是上圖中Github的認證頁面。
2.3 處理Github OAuth 2.0 的回調&保存Github的用戶信息
然后輸入賬號密碼登錄Github,隨后Github會采用OAuth 2.0的流程,重定向到oidc-server.test的回調地址上。
這個回調地址是標准的OAuth 2的流程,返回了code和state參數,OAuthHandler類的 protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync() 方法會根據code得到github的access_token,然后進一步的獲取到github的用戶信息(位於GithubOAuthHandler類)。
1 protected override async Task<AuthenticationTicket> CreateTicketAsync( 2 ClaimsIdentity identity, 3 AuthenticationProperties properties, 4 OAuthTokenResponse tokens) 5 { 6 var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, base.Options.UserInformationEndpoint); 7 httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); 8 var httpResponseMessage = await base.Backchannel.SendAsync(httpRequestMessage, base.Context.RequestAborted); 9 if (!httpResponseMessage.IsSuccessStatusCode) 10 { 11 throw new HttpRequestException($"An error occurred when retrieving Github user information ({httpResponseMessage.StatusCode})."); 12 } 13 var user = JObject.Parse(await httpResponseMessage.Content.ReadAsStringAsync()); 14 var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, base.Context, base.Scheme, base.Options, base.Backchannel, tokens, user); 15 context.RunClaimActions(); 16 await base.Events.CreatingTicket(context); 17 return new AuthenticationTicket(context.Principal, context.Properties, base.Scheme.Name); 18 }
隨后把這些信息加密保存到了名為“idsrv.external”(還記得在一開始的時候設置的options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme吧)的cookie中。
2.4. 根據保存的Github用戶信息查找已關聯的oidc-server.test的用戶(或新建)
在上一步保存完github的用戶信息到cookie中后,ids4便開始根據github的用戶信息查找是否已經綁定了已有的用戶,如果沒有則新建一個。我這里模擬了一個新建用戶的頁面(簡單的設置了下昵稱和用戶頭像-來自github):
隨后,ids4保存這個新用戶的信息,並且用它登錄系統(並清空保存的github的用戶信息)。
2.5 構造id_token & 重定向到客戶端
隨后的流程就和[OIDC in Action] 1. 基於OIDC(OpenID Connect)的SSO - 第5步時一樣的了,這里就不介紹了,完成后客戶端或得到了id_token,讀取到了其中的github的用戶信息。
總結
剖析oidc-server.test如何利用ids4來擴展第三方的登錄認證方式。文章中的例子是利用ids4來處理的,其他的比如node.js或者java等等平台,代碼也許不一樣,但是核心流程是一樣的:
- 即先使用github登錄,獲取到認證用戶的信息。
- 然后利用這些信息鏈接到自有賬號體系,最終使用自有的賬號體系完成認證。
- 擴展登錄的信息可以根據需要放到發放給客戶端的idtoken中,但是只是作為輔助信息存在的。
本例只是使用OAuth 2.0(IDP)作為了OIDC的OP,但是並不僅限於此,還支持SAML,WS-Federation,Windows AD,或者常用的手機短信驗證碼等等方式,其實OIDC並不關系是如何完成用戶認證的,它關心的只是得到用戶認證的信息后,按照統一的規范的流程把這個認證信息(id_token)安全的給到OIDC的客戶端即可。
如有錯誤指出,歡迎指正!
參考
idp vs op :http://lists.openid.net/pipermail/openid-specs/2006-November/003807.html
acr_values:http://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.1
github OAuth文檔:https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/
ids4 Sign-in with External Identity Providers:https://identityserver4.readthedocs.io/en/release/topics/signin_external_providers.html