[OIDC in Action] 3. 基於OIDC(OpenID Connect)的SSO(添加Github OAuth 2.0的支持)


在上上一篇基於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):

  1. oauth2.github.aspnetcore這個項目,它基於aspnetcore2實現了Github OAuth 2.0認證。
  2. oidc-server.test站點,對應的是web.oidc.server.ids4這個項目,引用了上面的這個項目。
  3. 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內部是怎么實現的呢?這里有兩件事情需要處理:

  1. oidc-server.test要能夠識別oidc客戶端傳遞過來的這個參數,如果參數有效,則使用參數指定的OP進行登錄,如果沒有指定,則采用默認的登錄方式(本地的用戶和密碼體系)。參數是 acr_values(Authentication Context Class Reference values),它是oidc協議規定的一個參數,Ids4實現了對這個參數的支持。
  2. 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項目中。我們暫且先不關注其內部是如何實現的,這里有兩個重要的信息。

  1. “github”,這是方法的第1個參數,指定了Github作為aspnetcore這個框架種支持的一種認證方式的唯一標識符,也就是一個scheme名字。
  2. 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等等平台,代碼也許不一樣,但是核心流程是一樣的:

  1. 即先使用github登錄,獲取到認證用戶的信息。
  2. 然后利用這些信息鏈接到自有賬號體系,最終使用自有的賬號體系完成認證。
  3. 擴展登錄的信息可以根據需要放到發放給客戶端的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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM