客户端凭证流程:理解整个设置的问题

Client Credentials flow: Problem understanding the whole setup

我目前正在尝试为我的公司设置一个基于 OpenIddict 的 AuthServer。我目前正在努力从我的一个测试 API 访问安全端点。

未来想要的解决方案

我想在长运行中实现的目标: 基于 OpenIddict 的集中式 Auth-Server,我的同事可以在其中注册他们的 APIs/clients/softwareprojects 以进行安全访问。为简单起见,我调用了我的同事 API1 的 API 之一。这个 API1 有一堆控制器和端点,需要使用 Auth-Server 进行保护。几个端点需要不同的保护级别。因此,API1 的客户端 1 仅获取读取端点的凭据,而客户端 2 也获取读取和写入端点的凭据。

现状与问题

现在我正尝试从一个简单的例子开始,我有 Auth-Server 和 API1,客户端现在是 Postman。

我在 Postman 中使用 OAuth2 方法获得了一个有效令牌,这是 Auth-Server 的日志:

    info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The request address matched a server endpoint: Token.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The token request was successfully extracted: {
        "grant_type": "client_credentials",
        "scope": "",
        "client_id": "resource_server_1",
        "client_secret": "[redacted]"
      }.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The token request was successfully validated.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The response was successfully returned as a JSON document: {
        "access_token": "[redacted]",
        "token_type": "Bearer",
        "expires_in": 3599
      }.

但是当我在 Postman 中使用此令牌访问我的 API1 中的授权端点时,我从 API1 收到此错误:

    info: OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandler[12]
      AuthenticationScheme: OpenIddict.Validation.AspNetCore was challenged.
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.LogicalHandler[100]
      Start processing HTTP request GET https://localhost:5001/.well-known/openid-configuration
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.ClientHandler[100]
      Sending HTTP request GET https://localhost:5001/.well-known/openid-configuration
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.ClientHandler[101]
      Received HTTP response after 326.6051ms - OK
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.LogicalHandler[101]
      End processing HTTP request after 366.9958ms - OK
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.LogicalHandler[100]
      Start processing HTTP request GET https://localhost:5001/.well-known/jwks
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.ClientHandler[100]
      Sending HTTP request GET https://localhost:5001/.well-known/jwks
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.ClientHandler[101]
      Received HTTP response after 76.1564ms - OK
info: System.Net.Http.HttpClient.OpenIddict.Validation.SystemNetHttp.LogicalHandler[101]
      End processing HTTP request after 83.7518ms - OK
info: OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandler[7]
      OpenIddict.Validation.AspNetCore was not authenticated. Failure message: An error occurred while authenticating the current request.
info: OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandler[7]
      OpenIddict.Validation.AspNetCore was not authenticated. Failure message: An error occurred while authenticating the current request.
info: OpenIddict.Validation.OpenIddictValidationDispatcher[0]
      The response was successfully returned as a JSON document: {
        "error": "server_error",
        "error_description": "This resource server is currently unavailable.",
        "error_uri": "https://documentation.openiddict.com/errors/ID2092"
      }.
info: OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandler[12]
      AuthenticationScheme: OpenIddict.Validation.AspNetCore was challenged.

Auth-Server 吐出这个:

    info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The request address matched a server endpoint: Configuration.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The configuration request was successfully extracted: {}.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The configuration request was successfully validated.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The response was successfully returned as a JSON document: {
        "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",
          "scp:profile"
        ],
        "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_basic",
          "client_secret_post"
        ],
        "claims_parameter_supported": false,
        "request_parameter_supported": false,
        "request_uri_parameter_supported": false
      }.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The request address matched a server endpoint: Cryptography.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The cryptography request was successfully extracted: {}.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The cryptography request was successfully validated.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
      The response was successfully returned as a JSON document: {
        "keys": [
          {
            "kid": "B0A4787E6E637564D164006A0E48F5C4FB1285BC",
            "use": "sig",
            "kty": "RSA",
            "alg": "RS256",
            "e": "AQAB",
            "n": "{VERY-LONG-STRING-HERE}",
            "x5t": "sKR4fm5jdWTRZABqDkj1xPsShbw",
            "x5c": [
              "{VERY-LONG-STRING-HERE}"
            ]
          }
        ]
      }.

代码

这是我从 Auth-Server 启动:

    using Cece.Server.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using static OpenIddict.Abstractions.OpenIddictConstants;

namespace Cece.Server
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

            services.AddDbContext<ApplicationDbContext>(options =>
            {
                // Configure the context to use Microsoft SQL Server.
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));

                // Register the entity sets needed by OpenIddict.
                // Note: use the generic overload if you need
                // to replace the default OpenIddict entities.
                options.UseOpenIddict();
            });

            services.AddOpenIddict()

                // Register the OpenIddict core components.
                .AddCore(options =>
                {
                    // Configure OpenIddict to use the Entity Framework Core stores and models.
                    // Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
                    options.UseEntityFrameworkCore()
                           .UseDbContext<ApplicationDbContext>();
                })

                // Register the OpenIddict server components.
                .AddServer(options =>
                {
                    // Enable the token endpoint.
                    options.SetTokenEndpointUris("/connect/token");

                    // Enable the client credentials flow.
                    options.AllowClientCredentialsFlow();

                    // Register the signing and encryption credentials.
                    options.AddDevelopmentEncryptionCertificate()
                           .AddDevelopmentSigningCertificate();

                    // Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
                    options.UseAspNetCore()
                           .EnableTokenEndpointPassthrough();

                    options.RegisterScopes(OpenIddictConstants.Permissions.Scopes.Profile);
                })

                // Register the OpenIddict validation components.
                .AddValidation(options =>
                {
                    // Import the configuration from the local OpenIddict server instance.
                    options.UseLocalServer();

                    // Register the ASP.NET Core host.
                    options.UseAspNetCore();
                });

            // Register the worker responsible of seeding the database with the sample clients.
            // Note: in a real world application, this step should be part of a setup script.
            services.AddHostedService<Worker>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseDeveloperExceptionPage();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(options =>
            {
                options.MapControllers();
                options.MapDefaultControllerRoute();
            });

            app.UseWelcomePage();
        }
    }
}

以及 Auth-Server 中的工作人员:

    using Cece.Server.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenIddict.Abstractions;
using System;
using System.Threading;
using System.Threading.Tasks;
using static OpenIddict.Abstractions.OpenIddictConstants;

namespace Cece.Server
{
    internal class Worker : IHostedService
    {
        private readonly IServiceProvider _serviceProvider;

        public Worker(IServiceProvider serviceProvider)
            => _serviceProvider = serviceProvider;

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

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

            await CreateApplicationsAsync();
            await CreateScopesAsync();

            async Task CreateApplicationsAsync()
            {
                var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();

                if (await manager.FindByClientIdAsync("cece") == null)
                {
                    var descriptor = new OpenIddictApplicationDescriptor
                    {
                        ClientId = "cece",
                        DisplayName = "Cece client application",
                        Permissions =
                        {
                            Permissions.Endpoints.Authorization,
                            Permissions.Endpoints.Logout,
                            Permissions.GrantTypes.ClientCredentials,
                            Permissions.ResponseTypes.IdToken,
                            Permissions.ResponseTypes.IdTokenToken,
                            Permissions.ResponseTypes.Token,
                            Permissions.Scopes.Email,
                            Permissions.Scopes.Profile,
                            Permissions.Scopes.Roles,
                            Permissions.Prefixes.Scope + "api1"
                        }
                    };

                    await manager.CreateAsync(descriptor);
                }

                if (await manager.FindByClientIdAsync("resource_server_1") == null)
                {
                    var descriptor = new OpenIddictApplicationDescriptor
                    {
                        ClientId = "resource_server_1",
                        ClientSecret = "My-Secret",
                        Permissions =
                        {
                            Permissions.GrantTypes.ClientCredentials,
                            Permissions.Endpoints.Token,
                            Permissions.Endpoints.Introspection
                        }
                    };

                    await manager.CreateAsync(descriptor);
                }

                // Note: no client registration is created for resource_server_2
                // as it uses local token validation instead of introspection.
            }

            async Task CreateScopesAsync()
            {
                var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictScopeManager>();

                if (await manager.FindByNameAsync("api1") == null)
                {
                    var descriptor = new OpenIddictScopeDescriptor
                    {
                        Name = "api1",
                        Resources =
                        {
                            "resource_server_1"
                        }
                    };

                    await manager.CreateAsync(descriptor);
                }
            }
        }

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

这是我从 API1 启动:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenIddict.Validation.AspNetCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Cece.TestApi1
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
            });

            // Register the OpenIddict validation components.
            services.AddOpenIddict()
                .AddValidation(options =>
                {
                    // Note: the validation handler uses OpenID Connect discovery
                    // to retrieve the address of the introspection endpoint.
                    options.SetIssuer("https://localhost:5001/");
                    options.AddAudiences("resource_server_1");

                    // Configure the validation handler to use introspection and register the client
                    // credentials used when communicating with the remote introspection endpoint.
                    options.UseIntrospection()
                           .SetClientId("resource_server_1")
                           .SetClientSecret("My-Secret");

                    // Register the System.Net.Http integration.
                    options.UseSystemNetHttp();

                    // Register the ASP.NET Core host.
                    options.UseAspNetCore();
                });

            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

问题

所以我的问题是:

  1. 我的上述设置是否正确?
  2. 我得到的这个错误到底是什么意思?

为了能够使用内省,您必须通过在服务器选项中给它一个地址来启用内省端点。解决这个问题,这个错误就会消失。