Search code examples
c#asp.netasp.net-coreblazorblazor-webassembly

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


Creating a new Blazor WebAssembly App with Microsoft Visual Studio 2019 Version 16.9.4 with these specifications: Target Framework .NET 5.0, Authentication Type Individual Accounts and ASP.NET Core Hosted:

enter image description here

Gives a Server project with these NuGets at version 5.0.5:

  • Microsoft.AspNetCore.ApiAuthorization.IdentityServer
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore
  • Microsoft.AspNetCore.Identity.UI

Startup.cs contains this code:

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

services.AddAuthentication()
    .AddIdentityServerJwt();

Reading the blog post ASP.NET Core Authentication with IdentityServer4 from Microsoft I should be able to retrieve a token with a sample request that looks like this:

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/

Creating a request that looks like that but for the solution created:

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

This request returns HTTP Status 400 Bad Request with body:

{
    "error": "unauthorized_client"
}

enter image description here

I'm pretty sure the values are correct since I got client_id and scope from the request used to sign in to the web application. That flow does not use grant_type=password though. Example request from login:

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

Confirmation that the user exists and works:

enter image description here

What am I missing?


Solution

  • TLDR:

    Remove this from appsettings.json:

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

    Edit Startup.cs:

    services.AddIdentityServer()
        .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
        {
            options.Clients.AddIdentityServerSPA("WebApplication4.Client", builder =>
            {
                builder.WithRedirectUri("/authentication/login-callback");
                builder.WithLogoutRedirectUri("/authentication/logout-callback");
            });
            //Or Duende.IdentityServer.Models.Client
            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" }
            });
        });
    

    This request will work:

    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
    

    Long answer:

    I started out with trying to get a better error messages with Logging.

    I added the code below to public static IHostBuilder CreateHostBuilder(string[] args) in Program.cs:

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

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

    When debugging I could then show output from the Server application when I made the request. It looked like this:

    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": "[email protected]",
              "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"
          }
    

    The error message to look at is Client not authorized for resource owner flow, check the AllowedGrantTypes setting{ client_id = WebApplication4.Client }.

    enter image description here

    With this error message I found this Question:

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

    There I could read

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

    Looking at appsettings.json the application uses that profile:

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

    Looking at Microsoft Application profiles what it actually does is this:

    • The redirect_uri defaults to /authentication/login-callback.
    • The post_logout_redirect_uri defaults to /authentication/logout-callback.
    • The set of scopes includes the openid, profile, and every scope defined for the APIs in the app.
    • The set of allowed OIDC response types is id_token token or each of them individually (id_token, token).
    • The allowed response mode is fragment.

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

    Before starting to modify this I visited the URL https://localhost:44388/.well-known/openid-configuration to get the current configuration. It looked like this and specifically says 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
    }
    

    For some reason IdentityServer Clients can not be configured in code and in appsettings.json. I therefore removed Clients from appsettings.json and added this to 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" }
            });
        });
    

    Without WithRedirectUri and WithLogoutRedirectUri it did not work, OidcConfigurationController got an exception for ClientRequestParametersProvider.GetClientParameters(HttpContext, clientId); with System.InvalidOperationException: 'Sequence contains no elements'. For some reason this is fixed automatically when using appsettings.json.

    I now got the error message when posting to /connect/token:

    {
        "error": "invalid_client"
    }
    

    But I got a much better error in the log:

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

    Added a secret to 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" }
            });
        });
    

    And the request:

    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
    

    It finally worked and the normal login flow worked as well!

    enter image description here