• [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

  • 相关阅读:
    Acquistion Location Confidence for accurate object detection
    Parallel Feature Pyramid Network for Object Detection
    第11组 Beta冲刺(3/5)
    第11组 Beta冲刺(2/5)
    第11组 Beta冲刺(1/5)
    第11组 Alpha事后诸葛亮
    第11组 Alpha冲刺(6/6)
    第11组 Alpha冲刺(5/6)
    第11组 Alpha冲刺(4/6)
    第11组 Alpha冲刺(3/6)
  • 原文地址:https://www.cnblogs.com/linianhui/p/oidc-in-action-extend-oidc-op-with-github.html
Copyright © 2020-2023  润新知