具有个人帐户和 ASP.NET 核心托管的 Blazor WebAssembly 应用程序 - 令牌请求 - "error":"unauthorized_client"

Blazor WebAssembly App with Individual Accounts and ASP.NET Core Hosted - Token request - "error": "unauthorized_client"

正在创建一个新的 Blazor WebAssembly AppMicrosoft Visual Studio 2019 Version 16.9.4 以及这些规格:Target Framework .NET 5.0Authentication Type Individual AccountsASP.NET Core Hosted

在版本 5.0.5:

中提供具有这些 NuGet 的服务器项目

Startup.cs 包含此代码:

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

services.AddAuthentication()
    .AddIdentityServerJwt();

阅读 Microsoft 的博客 post ASP.NET Core Authentication with IdentityServer4 我应该能够通过如下所示的示例请求检索令牌:

POST /connect/token HTTP/1.1
Host: localhost:5000
Cache-Control: no-cache
Postman-Token: 958df72b-663c-5638-052a-aed41ba0dbd1
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=Mike%40Contoso.com&password=MikesPassword1!&client_id=myClient&scope=myAPIs

https://devblogs.microsoft.com/aspnet/asp-net-core-authentication-with-identityserver4/

创建看起来像这样的请求,但针对创建的解决方案:

POST /connect/token HTTP/1.1
Host: localhost:44388
Content-Type: application/x-www-form-urlencoded
Content-Length: 153

grant_type=password&username=example%40example.com&password=Password1&client_id=WebApplication4.Client&scope=WebApplication4.ServerAPI%20openid%20profile

此请求 returns HTTP 状态 400 请求正文错误:

{
    "error": "unauthorized_client"
}

我很确定这些值是正确的,因为我从用于登录 Web 应用程序的请求中获得了 client_idscope。但是,该流程不使用 grant_type=password。来自登录的示例请求:

https://localhost:44388/Identity/Account/Login?ReturnUrl=/connect/authorize/callback?client_id=WebApplication4.Client&redirect_uri=https%3A%2F%2Flocalhost%3A44388%2Fauthentication%2Flogin-callback&response_type=code&scope=WebApplication4.ServerAPI%20openid%20profile&state=12345&code_challenge=12345&code_challenge_method=S256&response_mode=query

确认用户存在并在工作:

我错过了什么?

TLDR:

appsettings.json 中删除:

"Clients": {
  "WebApplication4.Client": {
    "Profile": "IdentityServerSPA"
  }
}

编辑Startup.cs:

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
    {
        options.Clients.AddIdentityServerSPA("WebApplication4.Client", builder =>
        {
            builder.WithRedirectUri("/authentication/login-callback");
            builder.WithLogoutRedirectUri("/authentication/logout-callback");
        });
        options.Clients.Add(new IdentityServer4.Models.Client
        {
            ClientId = "WebApplication4.Integration",
            AllowedGrantTypes = { GrantType.ResourceOwnerPassword },
            //Use Configuration.GetSection("MySecretValue").Value; to get a value from appsettings.json
            ClientSecrets = { new Secret("MySecretValue".Sha256()) },
            AllowedScopes = { "WebApplication4.ServerAPI", "openid", "profile" }
        });
    });

此请求有效:

POST /connect/token HTTP/1.1
Host: localhost:44388
Content-Type: application/x-www-form-urlencoded
Content-Length: 168

grant_type=password&username=example%40example.com&password=Password1!&client_id=WebApplication4.Integration&scope=WebApplication4.ServerAPI&client_secret=MySecretValue

长答案:

我开始尝试使用日志记录获得更好的错误消息。

我将下面的代码添加到 public static IHostBuilder CreateHostBuilder(string[] args)Program.cs:

.ConfigureLogging(logging =>
{
    logging.ClearProviders();
    logging.AddConsole();
})

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-5.0

调试时,我可以在发出请求时显示服务器应用程序的输出。它看起来像这样:

info: IdentityServer4.Hosting.IdentityServerMiddleware[0]
      Invoking IdentityServer endpoint: IdentityServer4.Endpoints.TokenEndpoint for /connect/token
info: IdentityServer4.Events.DefaultEventService[0]
      {
        "ClientId": "WebApplication4.Client",
        "AuthenticationMethod": "NoSecret",
        "Category": "Authentication",
        "Name": "Client Authentication Success",
        "EventType": "Success",
        "Id": 1010,
        "ActivityId": "8000000a-0000-8f00-b63f-84710c7967bb",
        "TimeStamp": "2021-04-29T11:47:07Z",
        "ProcessId": 8436,
        "LocalIpAddress": "::1:44388",
        "RemoteIpAddress": "::1"
      }
fail: IdentityServer4.Validation.TokenRequestValidator[0]
      Client not authorized for resource owner flow, check the AllowedGrantTypes setting{ client_id = WebApplication4.Client }, details: {
        "ClientId": "WebApplication4.Client",
        "ClientName": "WebApplication4.Client",
        "GrantType": "password",
        "Raw": {
          "grant_type": "password",
          "username": "example@example.com",
          "password": "***REDACTED***",
          "client_id": "WebApplication4.Client",
          "scope": "WebApplication4.ServerAPI"
        }
      }
info: IdentityServer4.Events.DefaultEventService[0]
      {
        "ClientId": "WebApplication4.Client",
        "ClientName": "WebApplication4.Client",
        "Endpoint": "Token",
        "GrantType": "password",
        "Error": "unauthorized_client",
        "Category": "Token",
        "Name": "Token Issued Failure",
        "EventType": "Failure",
        "Id": 2001,
        "ActivityId": "8000000a-0000-8f00-b63f-84710c7967bb",
        "TimeStamp": "2021-04-29T11:47:07Z",
        "ProcessId": 8436,
        "LocalIpAddress": "::1:44388",
        "RemoteIpAddress": "::1"
      }

要查看的错误消息是 Client not authorized for resource owner flow, check the AllowedGrantTypes setting{ client_id = WebApplication4.Client }

通过这个错误消息我发现了这个问题:

Question about ASP.NET Core 3 Identity / Identity Server / SPA support for Resource Owner Password Grant Type

在那里我可以阅读

found that the allowed grant type of password was not being added when the profile is set to IdentityServerSPA.

查看 appsettings.json 应用程序使用该配置文件:

"IdentityServer": {
  "Clients": {
    "WebApplication4.Client": {
      "Profile": "IdentityServerSPA"
    }
  }
},

看看 Microsoft Application profiles 它实际上是这样的:

  • redirect_uri默认为/authentication/login-callback。
  • post_logout_redirect_uri 默认为 /authentication/logout-callback.
  • 范围集包括openid、配置文件和每个范围 为应用程序中的 API 定义。
  • 允许的 OIDC 响应类型集是 id_token 令牌或每个 他们分别 (id_token, token).
  • 允许的响应模式是片段。

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-5.0#application-profiles

在开始修改之前,我访问了 URL https://localhost:44388/.well-known/openid-configuration 以获取当前配置。它看起来像这样,具体说 grant_types_supported: ...password:

{
    "issuer": "https://localhost:44388",
    "jwks_uri": "https://localhost:44388/.well-known/openid-configuration/jwks",
    "authorization_endpoint": "https://localhost:44388/connect/authorize",
    "token_endpoint": "https://localhost:44388/connect/token",
    "userinfo_endpoint": "https://localhost:44388/connect/userinfo",
    "end_session_endpoint": "https://localhost:44388/connect/endsession",
    "check_session_iframe": "https://localhost:44388/connect/checksession",
    "revocation_endpoint": "https://localhost:44388/connect/revocation",
    "introspection_endpoint": "https://localhost:44388/connect/introspect",
    "device_authorization_endpoint": "https://localhost:44388/connect/deviceauthorization",
    "frontchannel_logout_supported": true,
    "frontchannel_logout_session_supported": true,
    "backchannel_logout_supported": true,
    "backchannel_logout_session_supported": true,
    "scopes_supported": [
        "openid",
        "profile",
        "WebApplication4.ServerAPI",
        "offline_access"
    ],
    "claims_supported": [
        "sub",
        "name",
        "family_name",
        "given_name",
        "middle_name",
        "nickname",
        "preferred_username",
        "profile",
        "picture",
        "website",
        "gender",
        "birthdate",
        "zoneinfo",
        "locale",
        "updated_at"
    ],
    "grant_types_supported": [
        "authorization_code",
        "client_credentials",
        "refresh_token",
        "implicit",
        "password",
        "urn:ietf:params:oauth:grant-type:device_code"
    ],
    "response_types_supported": [
        "code",
        "token",
        "id_token",
        "id_token token",
        "code id_token",
        "code token",
        "code id_token token"
    ],
    "response_modes_supported": [
        "form_post",
        "query",
        "fragment"
    ],
    "token_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post"
    ],
    "id_token_signing_alg_values_supported": [
        "RS256"
    ],
    "subject_types_supported": [
        "public"
    ],
    "code_challenge_methods_supported": [
        "plain",
        "S256"
    ],
    "request_parameter_supported": true
}

由于某些原因 IdentityServer Clients 无法在代码和 appsettings.json 中进行配置。因此,我从 appsettings.json 中删除了 Clients 并将其添加到 Startup.cs:

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
    {
        options.Clients.AddIdentityServerSPA("WebApplication4.Client", builder =>
        {
            builder.WithRedirectUri("/authentication/login-callback");
            builder.WithLogoutRedirectUri("/authentication/logout-callback");
        });
        options.Clients.Add(new IdentityServer4.Models.Client
        {
            ClientId = "WebApplication4.Integration",
            AllowedGrantTypes = { GrantType.ResourceOwnerPassword },
            AllowedScopes = { "WebApplication4.ServerAPI", "openid", "profile" }
        });
    });

没有 WithRedirectUriWithLogoutRedirectUri 它不起作用,OidcConfigurationController 得到了 ClientRequestParametersProvider.GetClientParameters(HttpContext, clientId);System.InvalidOperationException: 'Sequence contains no elements' 的异常。出于某种原因,这在使用 appsettings.json.

时会自动修复

我现在在发帖到 /connect/token 时收到错误消息:

{
    "error": "invalid_client"
}

但是我在日志中得到了一个更好的错误:

Invalid client configuration for client WebApplication4.Integration: Client secret is required for password, but no client secret is configured.

Startup.cs 添加了一个秘密:

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
    {
        options.Clients.AddIdentityServerSPA("WebApplication4.Client", builder =>
        {
            builder.WithRedirectUri("/authentication/login-callback");
            builder.WithLogoutRedirectUri("/authentication/logout-callback");
        });
        options.Clients.Add(new IdentityServer4.Models.Client
        {
            ClientId = "WebApplication4.Integration",
            AllowedGrantTypes = { GrantType.ResourceOwnerPassword },
            //Use Configuration.GetSection("MySecretValue").Value; to get a value from appsettings.json
            ClientSecrets = { new Secret("MySecretValue".Sha256()) },
            AllowedScopes = { "WebApplication4.ServerAPI", "openid", "profile" }
        });
    });

请求:

POST /connect/token HTTP/1.1
Host: localhost:44388
Content-Type: application/x-www-form-urlencoded
Content-Length: 168

grant_type=password&username=example%40example.com&password=Password1!&client_id=WebApplication4.Integration&scope=WebApplication4.ServerAPI&client_secret=MySecretValue

终于成功了,正常的登录流程也成功了!