通过OpenIddict设计一个授权服务器03-客户凭证流程

在本部分中,我们将把 OpenIddict 添加到项目中,并实施第一个授权流程:客户端凭证流。

添加 OpenIddict 软件包

首先,我们需要安装 OpenIddict NuGet 软件包

dotnet add package OpenIddict
dotnet add package OpenIddict.AspNetCore
dotnet add package OpenIddict.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.InMemory

通过OpenIddict设计一个授权服务器03-客户凭证流程_第1张图片
除了主库,我们还安装了 OpenIddict.AspNetCore 软件包,该软件包可将 OpenIddict 集成到 ASPNET Core 主机中。
OpenIddict.EntityFrameworkCore 包支持 Entity Framework Core。现在我们将使用内存实现,为此我们使用了 Microsoft.EntityFrameworkCore.InMemory. 包。

设置 OpenIddict

我们将首先介绍启动和运行 OpenIddict 所需的最低条件。必须启用至少一个 OAuth 2.0/OpenID Connect 流程。我们选择启用客户端凭证流,它适用于机器到机器应用程序。在本系列的下一部分,我们将使用 PKCE 实现授权代码流,这是单页应用程序 (SPA) 和本地/移动应用程序的推荐流程。
开始对 Startup.cs 进行以下更改:

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
       .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
       {
           options.LoginPath = "/account/login";
       });
builder.Services.AddDbContext<DbContext>(options =>
{
    // 使用内存存储
    options.UseInMemoryDatabase(nameof(DbContext));

    // 注册OpenIddict所需的实体集。
    options.UseOpenIddict();
});
builder.Services.AddOpenIddict()
        // 注册 OpenIddict 核心组件
        .AddCore(options =>
        {
            // 配置 OpenIddict 以使用 EF Core 存储器/模型
            options.UseEntityFrameworkCore()
                .UseDbContext<DbContext>();
        })
        // 注册 OpenIddict 服务器组件
        .AddServer(options =>
        {
            options
                .AllowClientCredentialsFlow();

            options
                .SetTokenEndpointUris("/connect/token");

            //令牌的加密和签名
            options
                .AddEphemeralEncryptionKey()
                .AddEphemeralSigningKey();

            //注册范围(权限)
            options.RegisterScopes("api");

            //注册 ASP.NET Core 主机并配置 ASP.NET Core 特定选项 
            options
                .UseAspNetCore()
                .EnableTokenEndpointPassthrough();
        });

var app = builder.Build();

app.UseStaticFiles();

app.UseRouting();
app.UseAuthentication();
app.MapDefaultControllerRoute();
app.Run();

首先,在 ConfigureServices 方法中注册 DbContext。OpenIddict 原生支持 Entity Framework Core、Entity Framework 6 和 MongoDB,你也可以提供自己的存储。
在本例中,我们将使用 Entity Framework Core,并使用内存数据库。options.UseOpenIdDict 调用会注册 OpenIddict 所需的实体集。

接下来是注册 OpenIddict 本身。AddOpenIddict() 调用会注册 OpenIddict 服务,并返回一个 OpenIddictBuilder 类,我们可以用它来配置 OpenIddict。

首先注册的是核心组件。OpenIddict 被指示使用 Entity Framework Core,并使用前面提到的 DbContext。
接下来,注册服务器组件并启用客户端凭证流。为使该流程正常运行,我们需要注册一个令牌端点。我们需要自己实现这个端点。我们稍后再做这项工作。

要使 OpenIddict 能够加密和签名令牌,我们需要注册两个密钥,一个用于加密,一个用于签名。在本例中,我们将使用短暂密钥。短暂密钥会在应用程序关闭时自动丢弃,因此使用这些密钥签名或加密的有效负载会自动失效。这种方法只能在开发过程中使用。在生产过程中,建议使用 X.509 证书。

RegisterScopes 定义了支持哪些作用域(权限)。在本例中,我们只有一个名为 api 的作用域,但授权服务器可以支持多个作用域
UseAspNetCore() 调用用于将 AspNetCore 设置为 OpenIddict 的主机。我们还调用了 EnableTokenEndpointPassthrough,否则会阻止对未来令牌端点的请求。
要检查 OpenIddict 是否配置正确,我们可以启动应用程序并导航到:https://localhost:5001/.well-known/openid-configuration,得到的回应应该是这样的:

{
  "issuer": "https://localhost:5001/",
  "token_endpoint": "https://localhost:5001/connect/token",
  "jwks_uri": "https://localhost:5001/.well-known/jwks",
  "grant_types_supported": [
    "client_credentials"
  ],
  "scopes_supported": [
    "openid",
    "api"
  ],
  "claims_supported": [
    "aud",
    "exp",
    "iat",
    "iss",
    "sub"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "subject_types_supported": [
    "public"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_post",
    "private_key_jwt",
    "client_secret_basic"
  ],
  "claims_parameter_supported": false,
  "request_parameter_supported": false,
  "request_uri_parameter_supported": false,
  "authorization_response_iss_parameter_supported": true
}

通过OpenIddict设计一个授权服务器03-客户凭证流程_第2张图片
在本指南中,我们将使用 Postman 测试授权服务器,但也可以使用其他工具。
下面是一个使用 Postman 的授权请求示例。授权类型是客户凭据流。我们指定了访问令牌 url、客户端 ID 和秘密,以验证客户端身份。我们还请求访问 api 范围。
直接请求https://localhost:5001/connect/token
通过OpenIddict设计一个授权服务器03-客户凭证流程_第3张图片

如果我们请求令牌,操作将失败:client_id 无效。这是有道理的,因为我们还没有在授权服务器上注册任何客户端。
发送post请求
通过OpenIddict设计一个授权服务器03-客户凭证流程_第4张图片
用代码实现

using Flurl.Http;

namespace AuthClient
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var dic = new Dictionary<string, string>();
            dic.Add("grant_type", "client_credentials");
            dic.Add("client_id","postman");
            dic.Add("client_secret", "postman-secret");
            dic.Add("scope", "api");
            string url = "https://localhost:5001/connect/token";
            var response = url.PostUrlEncodedAsync(dic).Result;
            Console.WriteLine(response.ResponseMessage.Content.ReadAsStringAsync().Result);
            Console.WriteLine("完成");
        }
    }
}

我们可以通过将客户端添加到数据库来创建客户端。为此,我们创建了一个名为 TestData 的类。测试数据实现了 IHostedService 接口,这使我们能够在应用程序启动时在 Startup.cs 中执行生成测试数据的操作。

using Microsoft.EntityFrameworkCore;
using OpenIddict.Abstractions;

namespace AuthorizationServer
{
    public class TestData : IHostedService
    {
        private readonly IServiceProvider _serviceProvider;

        public TestData(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            using var scope = _serviceProvider.CreateScope();

            var context = scope.ServiceProvider.GetRequiredService<DbContext>();
            await context.Database.EnsureCreatedAsync(cancellationToken);

            var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();

            if (await manager.FindByClientIdAsync("postman", cancellationToken) is null)
            {
                await manager.CreateAsync(new OpenIddictApplicationDescriptor
                {
                    ClientId = "postman",
                    ClientSecret = "postman-secret",
                    DisplayName = "Postman",
                    Permissions =
                    {
                        OpenIddictConstants.Permissions.Endpoints.Token,
                        OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
                        OpenIddictConstants.Permissions.Prefixes.Scope + "api"
                    }
                }, cancellationToken);
            }
        }

        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    }
}

客户端在测试数据中注册。客户端 ID 和秘密用于客户端与授权服务器之间的身份验证。权限决定了客户端的选项。
在这种情况下,我们允许客户端使用客户端凭据流,访问令牌端点,并允许客户端请求 api 范围。
在 Startup.cs 中注册测试数据服务,以便在应用程序启动时执行:

builder.Services.AddHostedService<TestData>();

通过OpenIddict设计一个授权服务器03-客户凭证流程_第5张图片
如果我们再次尝试用 Postman 获取访问令牌,请求仍然会失败。这是因为我们还没有创建令牌端点。我们现在就创建。
通过OpenIddict设计一个授权服务器03-客户凭证流程_第6张图片

创建一个名为 AuthorizationController 的新控制器,我们将在此托管端点:

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using System.Security.Claims;

namespace AuthorizationServer.Controllers
{
    
    public class AuthorizationController : Controller
    {
        [HttpPost("~/connect/token")]
        public IActionResult Exchange()
        {
            var request = HttpContext.GetOpenIddictServerRequest() ??
                          throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

            ClaimsPrincipal claimsPrincipal;

            if (request.IsClientCredentialsGrantType())
            {
                // 注意:OpenIddict 会自动验证客户端凭证:
                // 如果 client_id 或 client_secret 无效,则不会调用此操作。
                var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

                // Subject (sub)是必填字段,我们在此使用客户 ID 作为主题标识符。
                identity.AddClaim(OpenIddictConstants.Claims.Subject, request.ClientId ?? throw new InvalidOperationException());

                // 添加一些要求,别忘了添加目的地,否则它不会被添加到访问令牌中。
                identity.AddClaim("some-claim", "some-value", OpenIddictConstants.Destinations.AccessToken);
                //上面这句话不起作用,下面的可以
                identity.AddClaim(new Claim("some-claim2", "some-value2").SetDestinations(OpenIddictConstants.Destinations.AccessToken));
                claimsPrincipal = new ClaimsPrincipal(identity);

                claimsPrincipal.SetScopes(request.GetScopes());
            }
            else
            {
                throw new InvalidOperationException("The specified grant type is not supported.");
            }

            // 返回 SignInResult 结果时,OpenIddict 将向用户发放相应的访问/身份令牌。
            return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }
    }
}

其中一个操作是 “Exchange”。所有流程(不仅是客户证书流程)都使用该操作来获取访问令牌。

在客户凭据流中,令牌是根据客户凭据签发的。而在授权码流中,使用的是同一个端点,但随后会用授权码来交换令牌。我们将在第四部分看到这一点。

目前,我们需要重点关注客户端凭证流程。当请求进入 Exchange 操作时,客户端凭证(ClientId 和 ClientSecret)已经通过 OpenIddict 验证。因此,我们不需要对请求进行验证,只需创建一个声称委托人并使用该委托人登录即可。

声明主体与账户控制器中使用的声明不同,后者基于 Cookie 身份验证处理程序,仅在授权服务器本身的上下文中使用,以确定用户是否已通过身份验证。

我们必须创建基于 OpenIddictServerAspNetCoreDefaults.AuthenticationScheme 的请求声明。这样,当我们在该方法末尾调用 SignIn 时,OpenIddict 中间件就会处理登录并返回一个访问令牌作为对客户端的响应。

只有当我们指定目的地时,才会在访问令牌中加入权利要求声明中定义的权利要求。示例中的 "some-value "请求将被添加到访问令牌中。
主题(Subject)要求是必填项,您无需指定目的地,因为它将包含在访问令牌中。

我们还通过调用 claimsPrincipal.SetScopes(request.GetScopes()); 授权所有请求的作用域。OpenIddict 已经检查了所请求的作用域是否被允许(一般情况下和针对当前客户端)。我们之所以要在此处手动添加作用域,是因为我们可以根据需要过滤此处授予的作用域。

一枚令牌定乾坤

让我们再次尝试用 Postman 获取访问令牌,这次应该能成功。
通过OpenIddict设计一个授权服务器03-客户凭证流程_第7张图片
从 OpenIddict v3 开始,访问令牌默认采用 Jason Web Token(JWT)格式。这使我们能够使用 jwt.io 检查令牌(感谢 Auth0 提供的服务!)。
一个问题是,令牌不仅要签名,还要加密。OpenIddict 默认会对访问令牌进行加密。我们可以在 Startup.cs 中配置 OpenIddict 时禁用这种加密。
通过OpenIddict设计一个授权服务器03-客户凭证流程_第8张图片
现在,当我们重启授权服务器并请求一个新的令牌时。将令牌粘贴到 jwt.io 并查看令牌内容:
通过OpenIddict设计一个授权服务器03-客户凭证流程_第9张图片

可以看到,客户 ID postman 被设置为Subject(sub)。此外,访问令牌中还添加了 some-claim claim。

接下来干什么

恭喜您,您已经使用 OpenIddict 实现了客户端凭证流!
您可能已经注意到,客户端凭证流未使用登录页面。该流程会立即将客户端凭据交换为令牌,适用于机器对机器应用程序。
接下来,我们将使用 PKCE 实现授权代码流,这是单页应用程序(SPA)和移动应用程序的推荐流程。该流程将涉及用户,因此我们的登录页面将发挥作用。

你可能感兴趣的:(dotnet,服务器,运维)