在上上一篇基于 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 的支持 https://github.com/linianhui/oidc.example/pull/14 .
由于 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 ):
这个项目, 它基于 aspnetcore2 实现了 GitHub OAuth 2.0 认证.
http://oidc-server.test/ 站点, 对应的是这个项目, 引用了上面的这个项目.
http://oidc-client-implicit.test/ 站点, 作为 oidc 的客户端, GitHub 登录的最终消费者(它无需关注 GitHub 登录的任何细节).
OIDC-Client :1 指定 oidc-server.test 使用 GitHub 认证(可选)
下图是上一篇中起始页面, 这次我们点击 Oidc Login(GitHub)这个链接(客户端也可以不指定采用 GitHub 进行认证, 推迟到进入 http://oidc-server.test/ 之后进行选择).
我们知道这个链接会返回一个 302 重定向, 重定向的地址是发往 http://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 内部是怎么实现的呢? 这里有两件事情需要处理:
http://oidc-server.test/ 要能够识别 oidc 客户端传递过来的这个参数, 如果参数有效, 则使用参数指定的 OP 进行登录, 如果没有指定, 则采用默认的登录方式(本地的用户和密码体系). 参数是 acr_values(Authentication Context Class Reference values), 它是 oidc 协议规定的一个参数, Ids4 实现了对这个参数的支持.
http://oidc-server.test/ 需要支持使用 GitHub 进行登录, 并且关联到 ids4 组件.
下面我们看看 http://oidc-server.test/ 这个站点是如何完成这两件事情的.
OIDC-Server : 1. 识别客户端发送的 IDP 信息
在 http://oidc-server.test/ 这个站点中, 在集成 ids4 组件的时候, 有这么一段代码:
- public static IServiceCollection AddIds4(this IServiceCollection @this)
- {
- @this
- .AddAuthentication()
- .AddQQConnect("qq", "QQ Connect", SetQQConnectOptions)
- .AddGithub("github", "Github", SetGithubOptions);
- @this
- .AddIdentityServer(SetIdentityServerOptions)
- .AddDeveloperSigningCredential()
- .AddInMemoryIdentityResources(Resources.AllIdentityResources)
- .AddInMemoryApiResources(Resources.AllApiResources)
- .AddInMemoryClients(Clients.All)
- .AddTestUsers(Users.All);
- return @this;
- }
- private static void SetGithubOptions(GithubOAuthOptions options)
- {
- options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
- options.ClientId = GlobalConfig.GitHub.ClientId;
- options.ClientSecret = GlobalConfig.GitHub.ClientSecret;
- }
AddGithub 这个扩展方法是我自己写的, 位于文章开始提到的项目中. 我们暂且先不关注其内部是如何实现的, 这里有两个重要的信息.
"github", 这是方法的第 1 个参数, 指定了 GitHub 作为 aspnetcore 这个框架种支持的一种认证方式的唯一标识符, 也就是一个 scheme 名字.
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; 其含义是把上面指定的 GitHub 这个认证方式, 作为 ids4 的外部登录来使用. 其实 ExternalCookieAuthenticationScheme 也是个字符串而已
publicconststring ExternalCookieAuthenticationScheme = "idsrv.external";
, 这个字符串是 ids4 定义的一个外部登录的 sheme 名字. 所有的外部登录如果想要和 ids4 集成, 都需要使用它来关联.
OIDC-Server : 2. 集成 GitHub 登录
有了上述两个信息, ids4 就可以在接收到 acr_values=idp:GitHub 这样的参数时, 就可以自动的从 aspnetcore 框架中已经注册的认证 scheme 中查找名为 gtihub 的认证方式, 然后来触 GitHub 登录的流程. 并且在 GitHub 认证完成后, 进入 ids4 定义的外部登录流程中. 从 Fiddler 中可以看到这个重定向的过程:
然后 GitHub 就打开了它的登录页面:
这部分的控制代码位于 GithubOAuthHandler 类继承的基类 BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) 方法中:
- protected virtual string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
- {
- var scopeParameter = properties.GetParameter<ICollection<string>>(OAuthChallengeProperties.ScopeKey);
- var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope();
- var state = Options.StateDataFormat.Protect(properties);
- var parameters = new Dictionary<string, string>
- {
- { "client_id", Options.ClientId },
- { "scope", scope },
- { "response_type", "code" },
- { "redirect_uri", redirectUri },
- { "state", state },
- };
- return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters);
- }
BuildChallengeUrl 方法返回的 URL 地址, 正是上图中 GitHub 的认证页面.
OIDC-Server : 3. 处理 GitHub OAuth 2.0 的回调 & 保存 GitHub 的用户信息
然后输入账号密码登录 GitHub, 随后 GitHub 会采用 OAuth 2.0 的流程, 重定向到 http://oidc-server.test/ 的回调地址上.
这个回调地址是标准的 OAuth 2 的流程, 返回了 code 和 state 参数, 类的 protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync() 方法会根据 code 得到 GitHub 的 access_token, 然后进一步的获取到 GitHub 的用户信息(位于 GithubOAuthHandler 类).
- protected override async Task<AuthenticationTicket> CreateTicketAsync(
- ClaimsIdentity identity,
- AuthenticationProperties properties,
- OAuthTokenResponse tokens)
- {
- var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, base.Options.UserInformationEndpoint);
- httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
- var httpResponseMessage = await base.Backchannel.SendAsync(httpRequestMessage, base.Context.RequestAborted);
- if (!httpResponseMessage.IsSuccessStatusCode)
- {
- throw new HttpRequestException($"An error occurred when retrieving Github user information ({httpResponseMessage.StatusCode}).");
- }
- var user = JObject.Parse(await httpResponseMessage.Content.ReadAsStringAsync());
- var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, base.Context, base.Scheme, base.Options, base.Backchannel, tokens, user);
- context.RunClaimActions();
- await base.Events.CreatingTicket(context);
- return new AuthenticationTicket(context.Principal, context.Properties, base.Scheme.Name);
- }
随后把这些信息加密保存到了名为 "idsrv.external"(还记得在一开始的时候设置的 options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme 吧)的 cookie 中.
OIDC-Server : 4. 根据保存的 GitHub 用户信息查找已关联的 oidc-server.test 的用户(或新建)
在上一步保存完 GitHub 的用户信息到 cookie 中后, ids4 便开始根据 GitHub 的用户信息查找是否已经绑定了已有的用户, 如果没有则新建一个. 我这里模拟了一个新建用户的页面(简单的设置了下昵称和用户头像 - 来自 GitHub):
随后, ids4 保存这个新用户的信息, 并且用它登录系统(并清空保存的 GitHub 的用户信息).
OIDC-Server : 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 作为了 OIDC 的 OP, 但是并不仅限于此, 还支持 SAML,WS-Federation,Windows AD, 或者常用的手机短信验证码等等方式, 其实 OIDC 并不关系是如何完成用户认证的, 它关心的只是得到用户认证的信息后, 按照统一的规范的流程把这个认证信息 (id_token) 安全的给到 OIDC 的客户端即可.
如有错误指出, 欢迎指正!
参考
- idp vs op :
- acr_values:
GitHub OAuth 文档:
ids4 Sign-in with External Identity Providers:
来源: https://www.cnblogs.com/linianhui/p/oidc-in-action-extend-oidc-op-with-github.html