5

As per Authentication and authorization for SPAs, I have created a new SPA with support for API authorization. You can view this on GitHub.

In order to support integration tests, I have added a new client (see appsettings.json) that is allowed the resource owner password grant type:

"SecureSpa.IntegrationTests": {
  "Profile": "IdentityServerSPA",
  "AllowedGrantTypes": [ "password" ],
  "ClientSecrets": [ { "Value": "K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=" } ],
  "AllowedScopes": [ "SecureSpaAPI", "openid", "profile" ]
}

Then within WeatherForecastControllerTests.cs, I attempt to request the token as follows:

var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
    Address = disco.TokenEndpoint,
    ClientId = "SecureSpa.IntegrationTests",
    ClientSecret = "secret",

    Scope = "SecureSpaAPI openid profile",
    UserName = "demouser@securespa",
    Password = "Pass@word1"
  });

When running the test, I've tried many different combinations, however the results are usually the same (unauthorized_client). This is the relevant log output from Identity Server:

IdentityServer4.Endpoints.TokenEndpoint: Debug: Start token request.
IdentityServer4.Validation.ClientSecretValidator: Debug: Start client validation
IdentityServer4.Validation.BasicAuthenticationSecretParser: Debug: Start parsing Basic Authentication secret
IdentityServer4.Validation.PostBodySecretParser: Debug: Start parsing for secret in post body
IdentityServer4.Validation.SecretParser: Debug: Parser found secret: PostBodySecretParser
IdentityServer4.Validation.SecretParser: Debug: Secret id found: SecureSpa.IntegrationTests
IdentityServer4.Stores.ValidatingClientStore: Debug: client configuration validation for client SecureSpa.IntegrationTests succeeded.
IdentityServer4.Validation.ClientSecretValidator: Debug: Public Client - skipping secret validation success
IdentityServer4.Validation.ClientSecretValidator: Debug: Client validation success
IdentityServer4.Events.DefaultEventService: Information: {
  "Name": "Client Authentication Success",
  "Category": "Authentication",
  "EventType": "Success",
  "Id": 1010,
  "ClientId": "SecureSpa.IntegrationTests",
  "AuthenticationMethod": "SharedSecret",
  "ActivityId": "0HLPN4PPDDMCJ",
  "TimeStamp": "2019-09-12T02:10:57Z",
  "ProcessId": 28948,
  "LocalIpAddress": "unknown",
  "RemoteIpAddress": "unknown"
}
IdentityServer4.Validation.TokenRequestValidator: Debug: Start token request validation
IdentityServer4.Validation.TokenRequestValidator: Debug: Start resource owner password token request validation
IdentityServer4.Validation.TokenRequestValidator: Error: Client not authorized for resource owner flow, check the AllowedGrantTypes setting{ client_id = SecureSpa.IntegrationTests }, details: {
  "ClientId": "SecureSpa.IntegrationTests",
  "ClientName": "SecureSpa.IntegrationTests",
  "GrantType": "password",
  "Raw": {
    "grant_type": "password",
    "username": "demouser@securespa",
    "password": "***REDACTED***",
    "scope": "SecureSpaAPI",
    "client_id": "SecureSpa.IntegrationTests",
    "client_secret": "***REDACTED***"
  }
}
IdentityServer4.Events.DefaultEventService: Information: {
  "Name": "Token Issued Failure",
  "Category": "Token",
  "EventType": "Failure",
  "Id": 2001,
  "ClientId": "SecureSpa.IntegrationTests",
  "ClientName": "SecureSpa.IntegrationTests",
  "Endpoint": "Token",
  "GrantType": "password",
  "Error": "unauthorized_client",
  "ActivityId": "0HLPN4PPDDMCJ",
  "TimeStamp": "2019-09-12T02:10:57Z",
  "ProcessId": 28948,
  "LocalIpAddress": "unknown",
  "RemoteIpAddress": "unknown"
}
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request finished in 212.96790000000001ms 400 application/json; charset=UTF-8

Is this approach supported? If not, is there an alternative approach that can be used to get the token in order to write integration tests? I'm planning to set up test users along with the test client so that I can test lots of different behaviours.

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
JasonTaylorDev
  • 121
  • 1
  • 6

2 Answers2

7

I continued working on this issue and found that the allowed grant type of password was not being added when the profile is set to IdentityServerSPA. I couldn't see a way to add a client without a profile via appsettings, so I removed the configuration from appsettings and created the clients using this approach:

services.AddIdentityServer()
    //.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
    {
        options.Clients.AddIdentityServerSPA("SecureSpa", builder =>
        {
            builder.WithRedirectUri("https://localhost:44307/authentication/login-callback");
            builder.WithLogoutRedirectUri("https://localhost:44307/authentication/logout-callback");
        });
        options.Clients.Add(new Client
        {
            ClientId = "SecureSpa.IntegrationTests",
            AllowedGrantTypes = { GrantType.ResourceOwnerPassword },
            ClientSecrets = { new Secret("secret".Sha256()) },
            AllowedScopes = { "SecureSpaAPI", "openid", "profile" }
        });
    });

With that in place my tests now run. You can see the final solution here; https://github.com/JasonGT/SecureSpa/.

Everything works fine, however there seems to be a bug (or feature limitation) within DefaultClientRequestParametersProvider. See the 'GetClientParameters' method - if the specified client does not have an associated profile, an InvalidOperationException is thrown.

Let me know if you need more information.

JasonTaylorDev
  • 121
  • 1
  • 6
0

Just for reference' sake: the code above did not work as-is on my end, it broke the SPA sign-in with a redirect_uri invalid exception.

I had to remove the base url, and then it worked:

builder.WithRedirectUri("/authentication/login-callback");
builder.WithLogoutRedirectUri("/authentication/logout-callback");