GVKun编程网logo

openID Connect 提供程序服务器中的 JWK 管理(openid connect java)

9

如果您想了解openIDConnect提供程序服务器中的JWK管理和openidconnectjava的知识,那么本篇文章将是您的不二之选。我们将深入剖析openIDConnect提供程序服务器中的J

如果您想了解openID Connect 提供程序服务器中的 JWK 管理openid connect java的知识,那么本篇文章将是您的不二之选。我们将深入剖析openID Connect 提供程序服务器中的 JWK 管理的各个方面,并为您解答openid connect java的疑在这篇文章中,我们将为您介绍openID Connect 提供程序服务器中的 JWK 管理的相关知识,同时也会详细的解释openid connect java的运用方法,并给出实际的案例分析,希望能帮助到您!

本文目录一览:

openID Connect 提供程序服务器中的 JWK 管理(openid connect java)

openID Connect 提供程序服务器中的 JWK 管理(openid connect java)

每个客户都应该有自己的 JWK。 OpenID Connect Dynamic Client Registration 1.0 将 jwksjwks_ur 定义为客户端元数据。

,

好的,我研究了一下,我得到的是这个:

  • Google 使用单个端点来获取其 JWK 并验证签名。不管是什么客户。

  • Auth0 为每个租户使用一个私钥,因此它为该租户中的所有客户端使用一个公钥/私钥。

  • Keycloak 每个领域使用一个密钥。因此该领域中的所有客户端都使用该单个公钥。

我的结论 如果我们的 OP 是多租户的,则每个租户的公钥/私钥可以是唯一的,否则对所有客户端使用单个公钥/私钥对。请注意,我们也应该有密钥轮换。

.conn.ManagedClientConnectionImpl@604ed9f0 java.net.ConnectException: Connection refused: connect

.conn.ManagedClientConnectionImpl@604ed9f0 java.net.ConnectException: Connection refused: connect

DEBUG 2016-11-07 14:32:47,518  Get connection for route {}->http://127.0.0.1:8087->http://rdsearch.zhaopin.com:80
DEBUG 2016-11-07 14:32:47,519  Connecting to 127.0.0.1:8087
DEBUG 2016-11-07 14:32:48,530  Connection org.apache.http.impl.conn.DefaultClientConnection@91161c7 closed
DEBUG 2016-11-07 14:32:48,531  Connection org.apache.http.impl.conn.DefaultClientConnection@91161c7 shut down
DEBUG 2016-11-07 14:32:48,532  Releasing connection org.apache.http.impl.conn.ManagedClientConnectionImpl@604ed9f0
java.net.ConnectException: Connection refused: connect
at java.net.DualStackPlainSocketImpl.connect0(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(Unknown Source)
at java.net.AbstractPlainSocketImpl.doConnect(Unknown Source)
at java.net.AbstractPlainSocketImpl.connectToAddress(Unknown Source)
at java.net.AbstractPlainSocketImpl.connect(Unknown Source)
at java.net.PlainSocketImpl.connect(Unknown Source)
at java.net.SocksSocketImpl.connect(Unknown Source)
at java.net.Socket.connect(Unknown Source)
at org.apache.http.conn.scheme.PlainSocketFactory.connectSocket(PlainSocketFactory.java:117)
at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:177)
at org.apache.http.impl.conn.ManagedClientConnectionImpl.open(ManagedClientConnectionImpl.java:304)
at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:611)
at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:446)
at org.apache.http.impl.client.AbstractHttpClient.doExecute(AbstractHttpClient.java:882)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:107)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:55)

.net – 使用DotNetOpenAuth的localhost上的OpenID提供程序

.net – 使用DotNetOpenAuth的localhost上的OpenID提供程序

我在本地运行DotNetopenAuth示例提供程序,它似乎通过Web浏览器正确处理请求.我可以在调试器中单步执行处理程序以进行授权.

我有一个项目可以与谷歌和其他提供商进行身份验证,但与样本提供商失败.样本提供程序根本看不到请求,依赖方抛出异常,抱怨找不到OpenID端点.

假设我在依赖方中执行以下操作:

string providerURL = "http://localhost/openid/provider";

// Now try the openid relying party...
var openid = new OpenIdRelyingParty();
var response = openid.GetResponse();
if (response == null)
{
    Identifier id;
    if (Identifier.TryParse(providerURL,out id))
    {
        // The following line throws the exception without ever making
        // a request to the server.
        var req = openid.CreateRequest(providerURL);
        // Would redirect here...
    }
 }

我注意到UntrustedWebRequestHandler类阻止了与诸如localhost之类的主机名的连接,但是根据测试用例或手动将其添加为列入白名单的主机似乎没有帮助.

我检查过主机是否可以通过以下方式访问:

// Check to make sure the provider URL is reachable.
// These requests are handled by the provider.
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(providerURL);
HttpWebResponse httpRes = (HttpWebResponse)request.GetResponse();

思考?我知道为什么它根本就没有提出要求.

编辑:localhost被列入白名单:

(openid.Channel.WebRequestHandler as UntrustedWebRequestHandler).WhitelistHosts.Add("localhost");

我也尝试通过将其添加到web.config来将其列入白名单,如下所示:

<dotNetopenAuth>
    <messaging>
        <untrustedWebRequest>
            <whitelistHosts>
                <add name="localhost"/>
            </whitelistHosts>
        </untrustedWebRequest>
    </messaging>
</dotNetopenAuth>

使用这两种方法,localhost在调试器中检查时显示在UntrustedWebRequestHandler的列入白名单的主机列表中.他们的提供者仍然没有收到任何请求.

解决方法

看起来您已经意识到需要将本地主机列入白名单以使其正常工作.但是我最近意识到的另一件事是IIS阻止ASP.NET Web应用程序自己执行HTTP GET.它适用于Visual Studio Personal Web Server,但如果您的RP和OP都托管在localhost下的IIS上,那么可能是它阻止了它的IIS.您可以通过使用IIS托管的RP与控制台应用程序中的手写HttpWebRequest测试来确认或拒绝此操作.

如果它们都在IIS下,那就是问题所在,那么您应该使用Personal Web Server进行开发,或者将IIS中的两个站点分离到不同的应用程序池中,或者这样做会有所帮助.

asp.net core 系列 56 IS4 使用 OpenID Connect 添加用户认证

asp.net core 系列 56 IS4 使用 OpenID Connect 添加用户认证

一。概述

  在前二篇中讲到了客户端授权的二种方式: GrantTypes.ClientCredentials 凭据授权和 GrantTypes.ResourceOwnerPassword 密码授权,都是 OAuth2.0 协议。本篇使用 OpenID Connect 添加用户认证,客户端授权是 GrantTypes.Implicit 隐式流授权,是 OCID 协议。 本篇示例中只有二个项目:一个 IdentityServer 的 mvc 应用程序,一个客户端 mvc 应用程序 (用户 client 端)。

    下面介绍身份认证交互流程:

      (1) 先启动 IdentityServer 程序 http://localhost:5000

      (2) 启动客户端 MvcClient 程序 http://localhost:5002

      (3) Client 用户访问 http://localhost:5002/Secure 时,想获取个人信息和资料信息,如果用户没有进行身份认证,OIDC 会重定向

      (4) 重定向到 IdentityServer 服务端站点的登录页:http://localhost:5000/Account/Login?ReturnUrl=xxx

  (5) 用户登录成功后。自动跳回到 MvcClient 客户端站点,访问地址 http://localhost:5002/Home/Secure。获取了当前个人信息和资料信息

  上面的步骤了解到:Client 用户要访问个人信息时,必须先进行,交互式用户身份验证 Account/Login,验证通过后,客户端浏览器会保存服务令牌在 cookie 中。 需要注意的是:在隐式授权中,令牌是通过浏览器传输,在 MvcClient 客户端程序中用 HttpClient 获取 cookie 中的令牌来请求 api, 返回是 http 401 状态,这是因为该令牌是身份令牌还非访问令牌。

   从 Github 中下载开源项目,可以快速入门启动 OpenID Connect 协议的交互式用户身份验证支持。在实际项目中,也可以将示例中的控制器,视图,模型和 CSS 整合到自己项目的 IdentityServer Web 应用程序中。

  

二. IdentityServer MVC 应用程序

   因为是交互式用户身份验证,必须有 UI 界面,所以 IdentityServer 是一个 MVC 应用程序。下面是示例项目目录:

    Account:客户端站点重定向到服务端站点,用于用户登录或注销。

    Grants: 用于撤销客户端访问权限。

    Consent :是用户登录成功后,跳转到授权许可的 UI 界面。用户可以决定是否要将他的身份信息发布到客户端应用程序。

    Device :是设备流交互服务。

    Diagnostics: 是诊断查看个人身份认证 cookie 信息。

 

  1.1 定义客户端

    在 config.cs 类中,定义客户端,将 OpenID Connect 隐式流添加到客户端。基于 OpenID Connect 的客户端与 OAuth 2.0 客户端非常相似。但由于 OIDC 中的流程始终是交互式的,因此我们需要在配置中添加一些重定向 URL。

public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientId = "client",

                    // no interactive user, use the clientid/secret for authentication
                    AllowedGrantTypes = GrantTypes.ClientCredentials,

                    // secret for authentication
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },

                    // scopes that client has access to
                    AllowedScopes = { "api1" }
                },
                // resource owner password grant client
                new Client
                {
                    ClientId = "ro.client",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                    AllowedScopes = { "api1" }
                },
                // OpenID Connect implicit flow client (MVC)
                new Client
                {
                    ClientId = "mvc",
                    ClientName = "MVC Client",
                    AllowedGrantTypes = GrantTypes.Implicit,
                     
                    //OIDC中的流程始终是交互式的
                    //登录后要重定向到哪里
                    RedirectUris = { "http://localhost:5002/signin-oidc" },

                    // 注销后重定向到哪里
                    PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },

                    //与OAuth 2.0类似,OpenID Connect也使用范围概念,与OAuth相比,OIDC中的范围不代表API,而是代表用户ID,名称或电子邮件地址等身份数据
                    AllowedScopes = new List<string>
                    {
                        //主题id,也是用户唯一ID(最低要求)
                        IdentityServerConstants.StandardScopes.OpenId,
                        //个人信息的claims,名称或电子邮件地址等身份数据
                        IdentityServerConstants.StandardScopes.Profile
                    }
                }
            };
        }

 

  1.2 定义 OIDC 范围

public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
            };
        }

  

  1.3 在 Startup 启动类中启动 IdentityServer 服务

var builder = services.AddIdentityServer()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApis())
                .AddInMemoryClients(Config.GetClients())
                .AddTestUsers(Config.GetUsers());    

 

二. MvcClient 客户端应用程序

  2.1 Startup 启动类

    添加对 OpenID Connect 身份验证的支持,在启动时将以下代码添加到 ConfigureServices 方法中:

public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            //关闭了JWT声明类型映射
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            //添加authentication 到服务集合中
            services.AddAuthentication(options =>
                {
                    //使用cookie本地登录用户
                    options.DefaultScheme = "Cookies";
                    //用户登录时,使用OpenID连接协议。
                    options.DefaultChallengeScheme = "oidc";
                })
                //添加对cookie的处理支持
                .AddCookie("Cookies")
                //oidc处理程序
                .AddOpenIdConnect("oidc", options =>
                {
                    //受信任的IdentityServer服务地址
                    options.Authority = "http://localhost:5000";
                    options.RequireHttpsMetadata = false;
                    //客户端标识
                    options.ClientId = "mvc";
                    //将IdentityServer中的令牌持久化到cookie中(客户端浏览器中)
                    options.SaveTokens = true;
                    
                });
        }
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            //每个请求都能执行身份验证服务
            app.UseAuthentication();

            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }    

  

  2.2 访问个人信息  

    由于使用的是 OpenID Connect,是基于浏览器的交互式身份认证。在 action 中添加一个 [Authorize], 会触发身份验证握手。下面 Secure 方法,显示当前用户的声明以及 cookie 属性。 握手时重定向到 IdentityServer 服务站点下进行登录。

//身份验证握手,采用oidc,重定向到IdentityServer进行登录
        [Authorize]
        public IActionResult Secure()
        {
            ViewData["Message"] = "Secure page.";

            return View();
        }

    下面是 Secure 视图:

@using Microsoft.AspNetCore.Authentication

<h2>Claims</h2>

<dl>
    <!-- 显示用户声明-->
    @foreach (var claim in User.Claims)
    {
        <dt>@claim.Type</dt>
        <dd>@claim.Value</dd>
    }
</dl>

<h2>Properties</h2>

<dl>
    <!-- 显示身份认中的cookie -->
    @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
    {
        <dt>@prop.Key</dt>
        <dd>@prop.Value</dd>
    }
</dl>

   

  2.3 注销

    使用 IdentityServer 等身份验证服务,仅清除本地应用程序 cookie 是不够的 (客户端浏览器)。此外,还需要向 IdentityServer 进行往返以清除中央单点登录会话。

    public IActionResult Logout()
    {
        return SignOut("Cookies", "oidc");
    }

    触发 Logout 后,会清除本地 cookie (客户端浏览器),然后重定向到 IdentityServer。IdentityServer 将清除其 cookie (服务端浏览器),然后为用户提供返回 MVC 应用程序的链接。

 

 参考文献

  使用 OpenID Connect 添加用户认证

 

原文出处:https://www.cnblogs.com/MrHSR/p/10723291.html

ASP.NET Core 认证与授权[3]:OAuth & OpenID Connect认证

ASP.NET Core 认证与授权[3]:OAuth & OpenID Connect认证

原文: ASP.NET Core 认证与授权[3]:OAuth & OpenID Connect认证

在上一章中,我们了解到,Cookie认证是一种本地认证方式,通常认证与授权都在同一个服务中,也可以使用Cookie共享的方式分开部署,但局限性较大,而如今随着微服务的流行,更加偏向于将以前的单体应用拆分为多个服务并独立部署,而此时,就需要一个统一的认证中心,以及一种远程认证方式,本文就来介绍一下如今最为流行的远程认证方式:OAuth 和 OpenID Connect。

目录

  1. OAuth 2.0
    • 用例
    • 源码分析
  2. OpenID Connect
    • 示例
    • 运行流程

OAuth 2.0

在介绍OAuth之前,我们先简单介绍一下OpenID。OpenID 是一个以用户为中心的数字身份识别框架,它具有开放、分散性。OpenID 的创建基于这样一个概念:我们可以通过 URI (又叫URL或网站地址)来认证一个网站的唯一身份,同理,我们也可以通过这种方式来作为用户的身份认证。

OpenID的认证非常简单,当你访问需要认证的A网站时,A网站要求你输入你的OpenID用户名,然后会跳转你的OpenID服务网站,输入用户名密码验证通过后,再跳回A网站,而些时已经显示认证成功。除了一处注册,到处通行外,OpenID还可以使所有支持OpenID的网站共享用户资源,而用户可以控制哪些信息可以被共享,例如姓名、地址、电话号码等。

而OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,在官网对其是这样定义的:

An open protocol to allow secure API authorization in a simple and standard method from desktop and web applications.

OAuth关注的是第三方应用访问其受保护资源的能力,而OpenID关注的是第三方应用获取用户身份的能力。

如今大多网站都已不再支持OpenID,最为流行的是OAuth 2.0 (在本文中提到OAuth也均指2.0版本),而OpenID的最新版是OpenID Connect,是OpenID的第三代技术,下文会来介绍。

关于OAuth的介绍,网上非常之多,本文就不再过多叙述,而是主要讲解如何在 ASP.NET Core 中使用 OAuth 认证。如果你对 OAuth 并不了解,那么建议先去网上查看一下这方面的资料,再来阅读本文。而在本文中提到的OAuth认证指的是 ASP.NET Core 中的一种认证方式,而OAuth本身只是一种授权协议,希望不要混淆。

在 OAuth 协议中包含以下四种授权模式:

  • 授权码模式(authorization code)

  • 简化模式(implicit)

  • 密码模式(resource owner password credentials)

  • 客户端模式(client credentials)

在以上四种模式中,只有第一种Code模式需要服务端参与,其它的只在客户端就可完成,因此,在 ASP.NET Core 的 OAuth 认证中,也就只有Code模式。

用例

先来看一下具体的用法:

对于项目的创建可以参考上一章,然后添OAuth的Nuget包引用:

dotnet add package Microsoft.AspNetCore.Authentication.OAuth --version 2.0.0

然后在ConfigureServices配置服务:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OAuthDefaults.DisplayName;
    })
    .AddCookie()
    .AddOAuth(OAuthDefaults.DisplayName, options =>
    {
        options.ClientId = "oauth.code";
        options.ClientSecret = "secret";
        options.AuthorizationEndpoint = "https://oidc.faasx.com/connect/authorize";
        options.TokenEndpoint = "https://oidc.faasx.com/connect/token";
        options.CallbackPath = "/signin-oauth";
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("email");
        options.SaveTokens = true;
        // 事件执行顺序 :
        // 1.创建Ticket之前触发
        options.Events.OnCreatingTicket = context => Task.CompletedTask;
        // 2.创建Ticket失败时触发
        options.Events.OnRemoteFailure = context => Task.CompletedTask;
        // 3.Ticket接收完成之后触发
        options.Events.OnTicketReceived = context => Task.CompletedTask;
        // 4.Challenge时触发,默认跳转到OAuth服务器
        // options.Events.OnRedirectToAuthorizationEndpoint = context => context.Response.Redirect(context.RedirectUri);
    });
}

上面前六个参数是都必填的(在IdentityServer中Scope必须包含openid),否则会报错,SaveTokens属性用来设置是否将OAuth服务器返回的Token信息保存到AuthenticationProperties中。

https://oidc.faasx.com 是我使用 IdentityServer4 搭建的一个OIDC服务,源码地址在 IdentityServerSample ,而本文中并不会涉及到IdentityServer的相关知识,后续有机会再来单独介绍一下它。

最后,注册中间件:

public void Configure(IApplicationBuilder app)
{
    app.UseAuthentication();

    // 授权,与上一章Cookie认证中的实现一样
    app.UseAuthorize();

    // 我的信息
    app.Map("/profile", builder => builder.Run(async context =>
    {
        await context.Response.WriteHtmlAsync(async res =>
        {
            await res.WriteAsync($"<h1>你好,当前登录用户: {HttpResponseExtensions.HtmlEncode(context.User.Identity.Name)}</h1>");
            await res.WriteAsync("<a class=\"btn btn-default\" href=\"/Account/Logout\">退出</a>");

            await res.WriteAsync($"<h2>AuthenticationType:{context.User.Identity.AuthenticationType}</h2>");

            await res.WriteAsync("<h2>Claims:</h2>");
            await res.WriteTableHeader(new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value }));

            // 在第一章中介绍过HandleAuthenticateOnceAsync方法,在此调用并不会有多余的性能损耗。
            var result = await context.AuthenticateAsync();
            await res.WriteAsync("<h2>Tokens:</h2>");
            await res.WriteTableHeader(new string[] { "Token Type", "Value" }, result.Properties.GetTokens().Select(token => new string[] { token.Name, token.Value }));
        });
    }));

    // 退出
    app.Map("/Account/Logout", builder => builder.Run(async context =>
    {
        await context.SignOutAsync();
        context.Response.Redirect("/");
    }));

    // 首页
    app.Run(async context =>
    {
        await context.Response.WriteHtmlAsync(async res =>
        {
            await res.WriteAsync($"<h2>Hello OAuth Authentication</h2>");
            await res.WriteAsync("<a class=\"btn btn-default\" href=\"/profile\">我的信息</a>");
        });
    });
}

在 上一章 中有介绍到远程认证并不具备SignIn/SignOut的功能,而在这里的context.SignOutAsync()方法是由 CookieHandler 来执行的,因为我们指定了options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme ,而DefaultSignOutScheme默认会使用DefaultSignInScheme中指定的值。

然后运行,访问:http://localhost:5001,点击 “我的信息” 按钮,将会跳转到OAuth服务器,登录成功后则显示授权页面:

oauth_consent

点击允许,跳回我们的网站,并已登录成功,显示如下:

oauth_profile

如图,Cliams中是空的,只有Token的相关信息,包含:access_token, token_type, expires_at 三个值。后续,可以使用access_token来访问OAuth服务方提供的受保护资源。这也就解释了为什么说OAuth只是授权,因为我们得到的只有一个本身无法识别的access_token,而没有关于用户身份的任何信息,这也正是OAuth的本意。而国内却大多使用OAuth来做认证,以至于大多人都认为OAuth指的是认证,而非授权。虽然OAuth后来补充了 RFC7662 - OAuth2 Token Introspection 协议,让我们可以获取到用户的身份,但是并不建议使用,而是使用下面要介绍的OpenID Connect来做身份的认证。

下面再来简单介绍一下其运行流程。

源码分析

AddOAuth与上一章中介绍的AddCookie实现逻辑类似,而OAuthOptions中的参数,都是OAuth中的标准参数,不用多说。主要来介绍一下OAuthHandler,其用来完成获取Code,再使用Code获取AccessToken的整个流程:

public class OAuthHandler<TOptions> : RemoteAuthenticationHandler<TOptions> where TOptions : OAuthOptions, new()
{
    ...

    protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
    {
        // 获取从OAuth服务器返回的state
        var state = query["state"];
        var properties = Options.StateDataFormat.Unprotect(state);

        // 获取从OAuth服务器返回的code
        var code =  query["code"];
        var tokens = await ExchangeCodeAsync(code, BuildRedirectUri(Options.CallbackPath));

        // ClaimsIssuer参数继承自父类:protected virtual string ClaimsIssuer => Options.ClaimsIssuer ?? Scheme.Name;
        var identity = new ClaimsIdentity(ClaimsIssuer);
        if (Options.SaveTokens)
        {
            // 保存Token信息到properties中,包括access_token refresh_token token_type expires_at
            properties.StoreTokens(authTokens);
        }
        var ticket = await CreateTicketAsync(identity, properties, tokens);
        return HandleRequestResult.Success(ticket);
    }

    // 使用上面获取到的授权码,拼装请求参数,然后调用TokenEndpoint,获取到Token。
    protected virtual async Task<OAuthTokenResponse> ExchangeCodeAsync(string code, string redirectUri) { }

    // 调用BuildChallengeUrl方法拼装请求参数,然后跳转。
    protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { }
}

补充一点,对于远程认证Handler,都只有请求路径(通常是认证服务器回调)与我们指定的CallbackPath一致时,才会执行,这一点在 第一章 中也有介绍过。

以上代码简单展示了OAuth授权码模式的基本实现,完整的代码在:OAuthHandler,总的来说,OAuth认证还是比较简单的,我在这里再简单叙述一下。

授权码模式的整个流程如下:

     +----------+
     | Resource |
     |   Owner  |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier      +---------------+
     |         -+----(A)-- & Redirection URI ---->|               |
     |  User-   |                                 | Authorization |
     |  Agent  -+----(B)-- User authenticates --->|     Server    |
     |          |                                 |               |
     |         -+----(C)-- Authorization Code ---<|               |
     +-|----|---+                                 +---------------+
       |    |                                         ^      v
      (A)  (C)                                        |      |
       |    |                                         |      |
       ^    v                                         |      |
     +---------+                                      |      |
     |         |>---(D)-- Authorization Code ---------''      |
     |  Client |          & Redirection URI                  |
     |         |                                             |
     |         |<---(E)----- Access Token -------------------''
     +---------+       (w/ Optional Refresh Token)

第一次未登录时访问,将会跳转到认证服务器并带上returnUrl参数,其附带 client_id, scope, response_type, redirect_uri, state 五个参数,请求报文如下(为方便展示,都会使用URLDecode解码):

GET /connect/authorize?client_id=oauth.code&scope=openid profile email&response_type=code&redirect_uri=http://localhost:5001/signin-oauth&state=CfDJ8B4XRZETkRhMt3mT9VduB8K32v-jJapr_X1RhEIiixwkk7L8krUsn32tBnyn3D0NX8PjPPpGtiAEG6O0bWI9ke42XhA0hrk-nI5nM86Fj9BDVQMoUwFJlrmT3QWBV7qTHWwPVWIXsK6lZR00owdKOqAL7g-9LjVv150V3NeBHD1P_Jp9xiK1sN_WywIbEUSwE_ut_c6w4V5nilEe6MqU-4JUoz5BTiqXDGG5kTd36ivGal4ihisn07csWFdodvC61A HTTP/1.1
Referer: http://localhost:5001/

其ReturnUrl的拼装代码如下:

protected virtual string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
    var scope = 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);
}

在OAuth服务登录成功后,返回如下:

HTTP/1.1 302 Found
Location: http://localhost:5001/signin-oauth?code=011e45b0f509969ac85aa69ab199636ddb33c13a06c711672b1be99509a5e205&scope=openid profile email&state=CfDJ8B4XRZETkRhMt3mT9VduB8K32v-jJapr_X1RhEIiixwkk7L8krUsn32tBnyn3D0NX8PjPPpGtiAEG6O0bWI9ke42XhA0hrk-nI5nM86Fj9BDVQMoUwFJlrmT3QWBV7qTHWwPVWIXsK6lZR00owdKOqAL7g-9LjVv150V3NeBHD1P_Jp9xiK1sN_WywIbEUSwE_ut_c6w4V5nilEe6MqU-4JUoz5BTiqXDGG5kTd36ivGal4ihisn07csWFdodvC61A&session_state=AwD762ldo1cgW58P0qKbK20amkXDIsIm_GMm8oTas0Q.05716f0d6ff235cbc38da1c1d0508fa5

state 是我们在上一步中保存的 propertiescode则是最重要的参数,用来获取 access_token,由服务端来执行:

protected virtual async Task<OAuthTokenResponse> ExchangeCodeAsync(string code, string redirectUri)
{
    var tokenRequestParameters = new Dictionary<string, string>()
    {
        { "client_id", Options.ClientId },
        { "redirect_uri", redirectUri },
        { "client_secret", Options.ClientSecret },
        { "code", code },
        { "grant_type", "authorization_code" },
    };
    var requestContent = new FormUrlEncodedContent(tokenRequestParameters);
    var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
    requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    requestMessage.Content = requestContent;
    var response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted);
    var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
    // payload:
    // {
    //     "id_token": "xxx",
    //     "access_token": "xxx",
    //     "expires_in": 3600,
    //     "token_type": "Bearer"
    // }
    return OAuthTokenResponse.Success(payload);
}

最后调用Cookie认证的Sign方法写入Cookie,并跳转到我的信息页面:

HTTP/1.1 302 Found
Location: http://localhost:5001/profile
Set-Cookie: .AspNetCore.Correlation.OAuth.In32Oho-aTNH4EOvCWNZYSs--sYgA3eRfoJC9tZkgBY=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/signin-oauth; samesite=lax
Set-Cookie: .AspNetCore.Cookies=CfDJ8B4XRZETkRhMt3mT9VduB8JypeVkaj-mkVB8iXvf2tG2d8Xfs5CoX2wugjdlUkBv2DY73FAvPwPJOo81GpKqRJjjYzgwemGkB98ZTN7dbKI9rT__Xwi-xsPZ-8gPCBtoeSnn1RfagM2kcprjk4djhBTrrJK1AVh3qufyu195Nju4Fqrmv91NKfelpztX0qaeVWS4y5cgbpKJwfeqJ3AfSSGkwnMFRIbKcX-TgJHKIDxQsP8OhAxm572GGv02X5WvpYZZF7Tc90zvNyH5HEzwv1nJ2yNuRUIgmSx6425M5RM684fq1fvaVIN29sORJCEj69gmw3xt7wpY3BsMYDyRXH1XSoa_n7WEjvnT6lfy1zYqSLDM0MAMOlkBzAXRQ5Vjr8IVhTPkGOEsxT0jqHeJrVOzYD5PcuveP-oey3-n7OJyrKOtByu5qUfzI2Gs_isUBiQSWcSfyQo9sLuB1Jj5dHtakCXOYUt8Hu2ysxUKugQiRSyh5WmxUm_RBuV8_QFzpD-3Ke5Brd1Kjl95Yo6iik66rfoHm1rcUOjKFGFvBl0be4uYz6-vhgauvhb-sa-gq-6uGLzNk_Qm9l-vf1nJk7h_qQ8OgwhjgNScrhMMt7r8DET3pX-_gYg7Dl56KBGCpVX6nrBFgjjrdpN6kvDh6v26zrEpgYW6IHqVkje8HMgZbWe2PdNflrA9DmV6XtOxncoLc3EusWmpkUk-FCXG3lIzd4VlC8iim7fbCJHd5Z6Cn6b9cRGTTf4juvUcOWvTbHXi6HkT2f1Ym9eFZ-7BBghRWwc2fERPgxPZEcwgkSdEgoHPq_eZtnshgHVTSM1e_FUyiZxh8miVJbRRhzWgdR5xNW--lOD6ShdNH6-22dKKOZbPdxTkxraZl9SXslTdR1ILoD4Z23Jyi-rRZ42uPzCIrX4PnJIzm9HjFvjGQJedL8mm2tDaIuYQ2_LBvyz8Wms9e0T_VXJCaf53IE2rqAKahwxAV7kRDudEPNIp4y7pJ7djdhEahtdVUwiIh4Pz9y1p74zA42HeI2lUn6pTTetH_npKn_dqu1puge_lXTncSH2yNqFZZTCQsNO6INjQjDMLRTkLGptQrjrMPz17MpFnb4lA1eVAo0R9EdlDIOAep2f2PuPzc-fVub5olnb8NjGUWy9J4rW6C-HBMj3sAlpZz9eHCYAOkRElKxeEgpyS1yOLA3469neukGKYFUySsTfDdEvU3JqsViFO3v_8EwCe80eiePQv1l4SF7AWwIqQhZuXf-n4_TXnoKFlZz9QqesA_npYg1LgnrhHhUXEAhHvyehEKLRXFUDMQqIls6b_WxWQ8d-9FBthl-WlZqMippZ1TJzGKLntsSXximbSnkGMuQfKNdESgIdUfvD1Dx8zbPVs_2U87slOiCAwbrXPi9oVIFj8OuTBFKaf6NY8hh2aQ9ywDlekGCujnnh6EzZoPHuCVCNQ1bqKzAxnvAZ6Eg; path=/; samesite=lax; httponly

OpenID Connect

OpenID Connect是OpenID的升级版,简称OIDC,是2014年初发布的开放标准,定义了一种基于OAuth2的可互操作的方式来来提供用户身份认证。在OIDC中,应用程序不必再为每个客户端构建不同的协议,而是可以将一个协议提供给多个客户端,它还使用了JOSN签名和加密规范,用来在传递携带签名和加密的信息,并使用简单的REST/JSON消息流来实现,和之前任何一种身份认证协议相比,开发者都可以轻松的集成。

上文中介绍到OAuth2是一个授权协议,它无法提供完善的身份认证功能。而OIDC使用OAuth2的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息传递给客户端,可以适用于各种类型的客户端(比如服务端应用,移动APP,JS应用),并完全兼容OAuth2,也就是说你搭建了一个OIDC的服务后,也可以当作一个OAuth2的服务来用(上面的OAuth服务器其实就是使用的OIDC服务器),应用场景如图:

oidc-protocols

OIDC在OAuth的基础上扩展了一些新的概念,避免了OAuth中的很多误区:

ID Tokens

OpenID Connect Id Token是一个签名的JSON Web Token(JWT:RFC7519),它包含一组关于用户身份的声明(claim),如:用户的标识(sub)、颁发令牌的提供程序的标识符(iss)、创建此标识的Client标识(aud),还包含token的有效期以及其他相关的上下文信息。

由于ID Token使用的是JWT签名,客户端可以直接解析出Token中的内容而无需依赖外部服务,因此我们可以使用ID Token来做身份认证,而不需要使用access_token。不过,OIDC为了保持于OAuth的兼容,会同时提供Id token和access_token。

UserInfo Endpoint

OIDC还提供了一个包含当前用户信息的标准的受保护的资源。UserInfo Endpoint不是身份认证的一部分,而是提供附加的标识信息,它提供了一组标准化的属性:比如profile、email、phone和address。OIDC中定义了一个特殊的openidscope,并且是必须的,它包含对Id tokenUserInfo Endpoint的访问权限。

服务发现和客户端注册

OIDC定义了一个发现协议,客户端可以自动的获取有关如何与身份认证提供者进行交互的信息,还定义了一个客户端注册协议,允许客户端引入新的身份提供程序(identity providers)。通过这两种机制和一个通用的身份API,OIDC可以在互联网规模上良好的运行,而不需要任何一方事先知道对方的存在。

Hybrid Flow

混合流模式是授权码模式与隐式模式的组合,在一次请求中可以同时获取Code和ID Token,ResponseType可以是:code id_token, code id_token tokencode token

简单的介绍了一下OIDC的基本概念(网上对于OIDC的介绍很多,本文就不多叙述),下面就来介绍一下 ASP.NET Core 中的OIDC认证。

示例

首先添OpenIdConnect的Nuget包引用:

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect --version 2.0.0

OIDC的注册其实要比OAuth还简单些:

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(o =>
{
    o.ClientId = "oidc.hybrid";
    o.ClientSecret = "secret";

    // 若不设置Authority,就必须指定MetadataAddress
    o.Authority = "https://oidc.faasx.com/";
    // 默认为Authority+".well-known/openid-configuration"
    //o.MetadataAddress = "https://oidc.faasx.com/.well-known/openid-configuration";
    o.RequireHttpsMetadata = false;

    // 使用混合流
    o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
    // 是否将Tokens保存到AuthenticationProperties中
    o.SaveTokens = true;
    // 是否从UserInfoEndpoint获取Claims
    o.GetClaimsFromUserInfoEndpoint = true;
    // 在本示例中,使用的是IdentityServer,而它的ClaimType使用的是JwtClaimTypes。
    o.TokenValidationParameters.NameClaimType = "name"; //JwtClaimTypes.Name;

    // 以下参数均有对应的默认值,通常无需设置。
    //o.CallbackPath = new PathString("/signin-oidc");
    //o.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
    //o.RemoteSignOutPath = new PathString("/signout-oidc");
    //o.Scope.Add("openid");
    //o.Scope.Add("profile");
    //o.ResponseMode = OpenIdConnectResponseMode.FormPost; 

    /***********************************相关事件***********************************/
    // 未授权时,重定向到OIDC服务器时触发
    //o.Events.OnRedirectToIdentityProvider = context => Task.CompletedTask;

    // 获取到授权码时触发
    //o.Events.OnAuthorizationCodeReceived = context => Task.CompletedTask;
    // 接收到OIDC服务器返回的认证信息(包含Code, ID Token等)时触发
    //o.Events.OnMessageReceived = context => Task.CompletedTask;
    // 接收到TokenEndpoint返回的信息时触发
    //o.Events.OnTokenResponseReceived = context => Task.CompletedTask;
    // 验证Token时触发
    //o.Events.OnTokenValidated = context => Task.CompletedTask;
    // 接收到UserInfoEndpoint返回的信息时触发
    //o.Events.OnUserInformationReceived = context => Task.CompletedTask;
    // 出现异常时触发
    //o.Events.OnAuthenticationFailed = context => Task.CompletedTask;

    // 退出时,重定向到OIDC服务器时触发
    //o.Events.OnRedirectToIdentityProviderForSignOut = context => Task.CompletedTask;
    // OIDC服务器退出后,服务端回调时触发
    //o.Events.OnRemoteSignOut = context => Task.CompletedTask;
    // OIDC服务器退出后,客户端重定向时触发
    //o.Events.OnSignedOutCallbackRedirect = context => Task.CompletedTask;

});

如上,ClientIdClientSecret与在OAuth中的作用一样,而在这里不需要再分别指定各种Endpoint,是通过指定一个MetadataAddress地址来自动发现,可访问 https://oidc.faasx.com/.well-known/openid-configuration 来了解一下MetadataAddress中包含的信息。

然后,在上文OAuth认证示例中的Configure方法基础上添加signoutsignout-remote

// 本地退出
app.Map("/signout", builder => builder.Run(async context =>
{
    await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    await context.Response.WriteHtmlAsync(async res =>
    {
        await res.WriteAsync($"<h1>Signed out {HttpResponseExtensions.HtmlEncode(context.User.Identity.Name)}</h1>");
        await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>");
    });
}));

// 远程退出
app.Map("/signout-remote", builder => builder.Run(async context =>
{
    await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties()
    {
        RedirectUri = "/signout"
    });
}));

如上,远程退出使用的是OpenIdConnectDefaults.AuthenticationScheme,而本地退出则使用的CookieAuthenticationDefaults.AuthenticationScheme

运行流程

登录

来探索一下OIDC的运行流程,首先运行,点击一下我的信息:

请求:
GET http://localhost:5002/profile HTTP/1.1

响应:
HTTP/1.1 302 Found
Location: https://oidc.faasx.com/connect/authorize?client_id=oidc.hybrid&redirect_uri=http://localhost:5002/signin-oidc&response_type=code id_token&scope=openid profile&response_mode=form_post&nonce=636428853279287956.N2IxZmFlZDgtNDNmZC00OTRmLTljMWItNTVjMmQwOTVjNTQ3MWUyMDcxNTctZDg4Yy00MDRiLThmNmQtYjE1YTdjOGE4MThm&state=CfDJ8B4XRZETkRhMt3mT9VduB8LSACJO9seruKlM3kYPxaRyWcUSt0BvPMd6RUGAiay8qraTWLdMh9B3ClRJDE-BtMRYTmzGJSHegueIW-fyq2G9TpUtSQCd23BxAYrdB4SeGQte2IXaQ82cKMz-aSHQ7TTzhPO_fgDtIVlwDJBtwgKQzEkEyyLsfH2DHxwr_Ojn3M-uRHId2bi9RF2gR_1hqoTdYlv-CZodFKuUGSMCqJO4cZLsuuAb-PrSnamz7h7MOpPixIOgQq5gd25sxF8avpSTsoT5HbU2fCiqX7g3rbCLzMG-rTnDftN8uZRiqc-JcyGkLPGIoj-FLNoW_yfZbGk&x-client-SKU=ID_NET&x-client-ver=2.1.4.0
Set-Cookie: .AspNetCore.OpenIdConnect.Nonce.CfDJ8B4XRZETkRhMt3mT9VduB8IHI9Q_BeT6uPrlI72UUqej78UfqAdiczZsPOxF3Gy7bSm7Swh9Jh0_haVi-UnQxUTGq-9xQLcaMX-PuXMjf6-6sLqc15NUgoLQ1w3KWqjkt7-3NlYW4qka6LqDPRtWJxT7vICtPhjx8ecNWtW_ijqBg_W8osmZLvFGS3PzPipP1UF14AkaIp48dFV1qNqt67Yta8ebXH7SHGkUhfcA5R-O0B8t-Q7sWL4NTN8AdKQF02HMqs_duf-4FLf2p7Rbpsc=N; expires=Fri, 06 Oct 2017 11:30:27 GMT; path=/signin-oidc; httponly
Set-Cookie: .AspNetCore.Correlation.OpenIdConnect.BqW1filmAVCStL92ghXFQZBoLQjG5-Gl_m60zxCW9BA=N; expires=Fri, 06 Oct 2017 11:30:27 GMT; path=/signin-oidc; httponly

由于我们还没有登录,会执行context.ChallengeAsync()方法,而我们在上面指定了DefaultChallengeScheme为:OpenIdConnectDefaults.AuthenticationScheme,因此会进入到OpenIdConnectHandler中来。

如上,重定向到了OIDC服务器,并写入了.AspNetCore.OpenIdConnect.Nonce.xxx.AspNetCore.Correlation.OpenIdConnect.xxx两个Cookie,前者是OIDC的标准,会包含在ID Token中,用来减缓重放攻击,后者由Options.CorrelationCookie.Name + Scheme.Name + "." + correlationId组成, 用于防止CSRF。

而重定向地址中包含如下几个参数:

  • client_id 客户端标识,对应于OpenIdConnectOptions.ClientId

  • redirect_uri 回调地址,对应于OpenIdConnectOptions.CallbackPath

  • response_type 授权类型,对应于OpenIdConnectOptions.ResponseType

  • scope 权限范围,对应于OpenIdConnectOptions.Scope

  • response_mode 响应模式,对应于OpenIdConnectOptions.ResponseMode,表示OIDC服务器来跳转到我们的应用时传递参数的方式。

  • nonce OIDC服务器会在identity token中包含此参数,在认证时与Cookie中的.AspNetCore.OpenIdConnect.Nonce.xxx对比验证。

  • state 用于保存状态,会原封不动地返回,在 ASP.NET Core 中,用AuthenticationProperties对象来表示。

  • x-client-SKU/x-client-ver IdentityServer附加信息。

在OIDC服务器登录成功后,connect/authorize输出的是一个自动提交的表单:

<form method=''post'' action=''http://localhost:5002/signin-oidc''>
<input type=''hidden'' name=''code'' value=''eb53860906da276a1bb5318c5d539db085c6ca1fd3467da9d918aa7524f20f63'' />
<input type=''hidden'' name=''id_token'' value=''eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlYzk5MjVlMmUzMTA2NmY2ZmU2ODgzMDRhZjU1ZmM0IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MDcyODg1MjcsImV4cCI6MTUwNzI4ODgyNywiaXNzIjoiaHR0cDovL29pZGMuZmFhc3guY29tIiwiYXVkIjoib2lkYy5oeWJyaWQiLCJub25jZSI6IjYzNjQyODg1MzI3OTI4Nzk1Ni5OMkl4Wm1GbFpEZ3RORE5tWkMwME9UUm1MVGxqTVdJdE5UVmpNbVF3T1RWak5UUTNNV1V5TURjeE5UY3RaRGc0WXkwME1EUmlMVGhtTm1RdFlqRTFZVGRqT0dFNE1UaG0iLCJpYXQiOjE1MDcyODg1MjcsImNfaGFzaCI6ImJNT1FNVGdDV3VMX25EbHo5MDU4M3ciLCJzaWQiOiJjZTVjODg2ZmFhZDFiNTc4MDVkNzExYjkzZTliYWQ0ZiIsInN1YiI6IjAwMSIsImF1dGhfdGltZSI6MTUwNzI4ODM5NSwiaWRwIjoibG9jYWwiLCJhbXIiOlsicHdkIl19.BBn0SigvOW9USk-Mi1WP_lJPWI9I06gsuomhqp69Ip5y3kqFyiBCVanzULsR4fBa0tOOBOtcPJzAfivzLqsMRwW1QfRamfVIlXuuzcRsR8WP1pFxvFPekwKi-D6-RLmmQzUT-_78WvboiAu_dZtwe0cm4ZLDCJH6LLCPs2xXTHYuNI7YoyAgeGKDAhWle0VrsbdlrcubPPgQFfFXPdLDInnLr8eEMpUZ7nru0FJgxm3Ah4hGXPKMud8jhLUMXDcSaKseL8tDgxIowmhpXOknU-y9x5FlrZUFOReDxaBZe7DG5V0xsPrdhMxMkZQbHHz8cJoaYrqcwHClm8rScEPxVA'' />
<input type=''hidden'' name=''scope'' value=''openid profile'' />
<input type=''hidden'' name=''state'' value=''CfDJ8B4XRZETkRhMt3mT9VduB8LSACJO9seruKlM3kYPxaRyWcUSt0BvPMd6RUGAiay8qraTWLdMh9B3ClRJDE-BtMRYTmzGJSHegueIW-fyq2G9TpUtSQCd23BxAYrdB4SeGQte2IXaQ82cKMz-aSHQ7TTzhPO_fgDtIVlwDJBtwgKQzEkEyyLsfH2DHxwr_Ojn3M-uRHId2bi9RF2gR_1hqoTdYlv-CZodFKuUGSMCqJO4cZLsuuAb-PrSnamz7h7MOpPixIOgQq5gd25sxF8avpSTsoT5HbU2fCiqX7g3rbCLzMG-rTnDftN8uZRiqc-JcyGkLPGIoj-FLNoW_yfZbGk'' />
<input type=''hidden'' name=''session_state'' value=''g0m_0W2scFKHxVQP_8jdNt48r2XV8wDlF__-ST9aYtk.28e1d34baad6d122a92c667329084600'' />
</form>
<script>(function(){document.forms[0].submit();})();</script>

如上,可以看到,表单中包含有id_token,因为我们使用的是code id_token类型,然后便进入到了我们应用程序的OIDC认证逻辑中:

首先通过IdToken,可以来解析出AuthenticationPropertiesClaimsPrincipal等信息,然后使用Code,调用TokenEndpoint,获取access_token等信息:

{
    "id_token": "....",
    "access_token": "...",
    "expires_in": 3600,
    "token_type": "Bearer"
}

因为我们将SaveTokens设置为true,则会将token信息保存到AuthenticationProperties中来:

public class OpenIdConnectHandler : RemoteAuthenticationHandler<OpenIdConnectOptions>, IAuthenticationSignOutHandler
{
    protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
    {
        ...
        if (Options.SaveTokens)
        {
            SaveTokens(properties, tokenEndpointResponse ?? authorizationResponse);
        }
        ...
    }

    private void SaveTokens(AuthenticationProperties properties, OpenIdConnectMessage message)
    {
        var tokens = new List<AuthenticationToken>();
        if (!string.IsNullOrEmpty(message.AccessToken))
        {
            tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = message.AccessToken });
        }
        if (!string.IsNullOrEmpty(message.IdToken))
        {
            tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = message.IdToken });
        }
        if (!string.IsNullOrEmpty(message.RefreshToken))
        {
            tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = message.RefreshToken });
        }
        if (!string.IsNullOrEmpty(message.TokenType))
        {
            tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.TokenType, Value = message.TokenType });
        }
        if (!string.IsNullOrEmpty(message.ExpiresIn))
        {
            if (int.TryParse(message.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
            {
                var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
                tokens.Add(new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) });
            }
        }
        properties.StoreTokens(tokens);
    }
}

当我们做身份验证时,可能会需要更详细的用户Claims,可以将GetClaimsFromUserInfoEndpoint设置为True,使用UserInfoEndpoint返回的信息来重新创建ClaimsPrincipal对象:

protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
{
    ...

    if (Options.GetClaimsFromUserInfoEndpoint)
    {
        return await GetUserInformationAsync(tokenEndpointResponse ?? authorizationResponse, jwt, user, properties);
        // UserInfoEndpoin返回的信息如下:
        // {
        //  "name": "Alice Smith",
        //  "given_name": "Alice",
        //  "family_name": "Smith",
        //  "website": "http://alice.com",
        //  "sub": "001"
        // }
    }
    else
    {
        var identity = (ClaimsIdentity)user.Identity;
        foreach (var action in Options.ClaimActions)
        {
            action.Run(null, identity, ClaimsIssuer);
        }
    }
    return HandleRequestResult.Success(new AuthenticationTicket(user, properties, Scheme.Name));
}

最后调用CookieHandler的SignInAsync方法,将AuthenticationTicket写入到Cookie中,响应如下:

HTTP/1.1 302 Found
Location: http://localhost:5002/profile
Set-Cookie: .AspNetCore.Correlation.OpenIdConnect.02q09RpgJBAi3rGZwy0WiyHUWGgLuDQIbVRUJuEXYBw=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/signin-oidc; samesite=lax
Set-Cookie: .AspNetCore.OpenIdConnect.Nonce.xxx=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/signin-oidc; samesite=lax
Set-Cookie: .AspNetCore.Cookies=chunks-2; path=/; samesite=lax; httponly
Set-Cookie: .AspNetCore.CookiesC1=xxx; path=/; samesite=lax; httponly
Set-Cookie: .AspNetCore.CookiesC2=xxx; path=/; samesite=lax; httponly

浏览器中显示如下:

oidc-claims

接下来可以使用Token中的access_token访问OIDC中的受保护资源(与OAuth用法一样),也可用使用Claims进行授权。可以看出,OIDC的认证流程比OAuth更加便捷和严谨,感兴趣的可以查看 OpenIdConnectHandler 的源码来更深一步的了解。

退出

退出分为两种,一种是本地的退出,并不会使OIDC服务器退出,只需要简单的调用CookieHandler的SignOutAsync即可,无需多说。

而远程退出则会同时退出本地应用和OIDC服务器,大致逻辑是先跳转到OIDC服务器,退出后,OIDC服务器会回调本地应用,完成本地的退出,该回调地址是通过OpenIdConnectOptions.RemoteSignOutPath指定的。

当我们点击退出时,首先执行OpenIdConnectHandler的SignOutAsync方法,Http报文如下:

请求报文:
GET http://localhost:5002/signout-remote HTTP/1.1
Cookie: .AspNetCore.Cookies=xxx

响应报文:
HTTP/1.1 302 Found
Location: http://oidc.faasx.com/connect/endsession?post_logout_redirect_uri=http://localhost:5002/signout-callback-oidc&id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlYzk5MjVlMmUzMTA2NmY2ZmU2ODgzMDRhZjU1ZmM0IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MDc0MjgzMTAsImV4cCI6MTUwNzQyODYxMCwiaXNzIjoiaHR0cDovL29pZGMuZmFhc3guY29tIiwiYXVkIjoib2lkYy5oeWJyaWQiLCJub25jZSI6IjYzNjQzMDI1MTExOTUyNjYxNS5NVFU0WXpNMFl6RXRZemcwWmkwME5UWTFMVGcyTW1ZdE5XUmtZbUZtTUdabU5UWXlaalF6TkRReE9HWXROalEwWXkwMFpHTmtMVGhqTlRFdE1XWTROV0V3TW1NM09UZG0iLCJpYXQiOjE1MDc0MjgzMTAsImF0X2hhc2giOiJQSHRvd1JrUHhYLWNsclBzRnYxMkpnIiwic2lkIjoiMzFmMWJmMTA1MTU4MmZjMGE3ZTZhZjFjY2I0Y2RlMzUiLCJzdWIiOiIwMDEiLCJhdXRoX3RpbWUiOjE1MDc0MjgzMTAsImlkcCI6ImxvY2FsIiwiYW1yIjpbInB3ZCJdfQ.hIxkDhsx_WE6IxM68O7uqkqdQquXXnOtxhlnrYiBJuU7Ex_aApVXoKUdHS8HMx1nLswntr6SRsrygyMJnGMdzP5JutGsmfO_i1WYGqk3BlTD7ry0wfBd_U9OaVFcJhcVZq4q5u3SA47Wxqex9vifiHrTBQFT_l6JqpevRLn-y91IxTl9rnXKfrHowhPsHJjdLzda3Lyj0wWWtb2N_ng19mRChmDd4RXucP9mBHdQDyLZtvIJ5iIzV4pqtL7VCylFzV4RBLbCzUeuRnHI3E_MqTaGWvVyFbUAKpr55TmVoc6lAOS4ie4CPzilR52KLWZ4l9eQh-WeIOA3NBgzHlJigg&state=CfDJ8B4XRZETkRhMt3mT9VduB8IbG5c9um6S_maiHLsMKlIFRVtKMyLuXfqgB6e1OhWWVVJDYbgedt6hmyi1ny2aMbKW-SgHTru73YezUAZpre2ELXM3trlnX3YW_FTkcGE_RUyaR3hQ3eEYFmMdgdZdf0M&x-client-SKU=ID_NET&x-client-ver=2.1.4.0

然后浏览器跳转到OIDC服务器,显示如下:

oidc_logout

其HTML中还嵌套一个IFrame:

<iframe width="0" height="0"src="http://oidc.faasx.com/connect/endsession/callback?endSessionId=CfDJ8ADnXgOvSAFKqD4LwO6fek3_sYV1rgqtFD-CM4iSTjIo5wVq7lP0euy9tskf5BZ5hJHGweIMBQcnOcc4UR35xe94aaywrULsbUfA8n_qNkVvtJbU0-EKMG2cadbhc6AHm06yyr8WpPEhZUvcVwlBFWNYnU9X6KErwyTE3oEe0yx-mOxWacIwWUbblQRjElil6PXICoR-0J6I4GfkPFRyHyja4EJz4IK_Ik-vr1Lw_CoAbpSQij2eIj54HfeE41TBJceoMNGJq8hJO_ybL-CKma1hNJ_sQ3Jc9h5uS8Y5oEig"></iframe>

该IFrame中的内容如下:

<!DOCTYPE html>
<html>
<style>iframe{display:none;width:0;height:0;}</style>
<body>
<iframe src=''http://localhost:5002/signout-oidc?sid=854b9825a8cf571f7995e1ebafde8d37&iss=http%3A%2F%2Foidc.faasx.com''>
</iframe>
</body></html>

在用户毫无感觉的情况下,调用我们的应用服务器中配置的回调地址OpenIdConnectOptions.RemoteSignOutPath,清除本地的Cookie,实现同步退出:

protected virtual async Task<bool> HandleRemoteSignOutAsync()
{
    OpenIdConnectMessage message = null;

    if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
    {
        message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
    }

    ...

    var remoteSignOutContext = new RemoteSignOutContext(Context, Scheme, Options, message);    
    await Events.RemoteSignOut(remoteSignOutContext);

    ...

    await Context.SignOutAsync(Options.SignOutScheme);
    return true;
}

而上图中的here链接则跳转到OpenIdConnectOptions.SignedOutCallbackPath,执行HandleSignOutCallbackAsync方法,我们可以通过注册事件的方式来附加一些业务逻辑:

 protected async virtual Task<bool> HandleSignOutCallbackAsync()
{
    var message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));

    ...

    var signOut = new RemoteSignOutContext(Context, Scheme, Options, message) { Properties = properties };

    await Events.SignedOutCallbackRedirect(signOut);

    ...

    if (!string.IsNullOrEmpty(properties?.RedirectUri))
    {
        Response.Redirect(properties.RedirectUri);
    }
    return true;
}

总结

本文简单介绍了OAuth和OpenID Connect的基本概念以及它们在 ASP.NET Core 中作为认证客户端的实现,如果我们只需要 "访问第三方资源" 的授权,使用OAuth认证即可。而在我们需要对自己的多个应用进行统一的身份验证时,应该使用OpenID Connect来实现,OpenID Connect不仅包含身份验证,还包含OAuth的授权协议,是更加推荐的做法。在下一章中来介绍一下另一种本地认证方式:JWTBearer,也是在现代Web应用中比较流行的认证方式。

附本文示例代码:https://github.com/RainingNight/AspNetCoreSample/tree/master/src/Functional/Authentication/OIDCSample/Startup.cs。

关于openID Connect 提供程序服务器中的 JWK 管理openid connect java的介绍现已完结,谢谢您的耐心阅读,如果想了解更多关于.conn.ManagedClientConnectionImpl@604ed9f0 java.net.ConnectException: Connection refused: connect、.net – 使用DotNetOpenAuth的localhost上的OpenID提供程序、asp.net core 系列 56 IS4 使用 OpenID Connect 添加用户认证、ASP.NET Core 认证与授权[3]:OAuth & OpenID Connect认证的相关知识,请在本站寻找。

本文标签: