Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IdentityServer4 Introspection Endpoint API uses invalid hashing algorithm

Trying to validate a token using Introspection Endpoint on IdentityServer4. I keep getting a 401:Unauthorized. My log looks like this:

dbug: IdentityServer4.EntityFramework.Stores.ResourceStore[0]
      Found MyAPI API resource in database
info: IdentityServer4.Validation.HashedSharedSecretValidator[0]
      Secret: MyAPI API uses invalid hashing algorithm.
dbug: IdentityServer4.Validation.SecretValidator[0]
      Secret validators could not validate secret
fail: IdentityServer4.Validation.ApiSecretValidator[0]
      API validation failed.
fail: IdentityServer4.Endpoints.IntrospectionEndpoint[0]
      API unauthorized to call introspection endpoint. aborting.

My API is configured like so:

new ApiResource
                {
                    Name = "MyAPI",
                    DisplayName = "My API",
                    ApiSecrets =
                    {
                        new Secret("TopSecret".Sha256())
                    },
                }

I'm passing headers for content type as application/x-www-form-urlencoded and authorization as Basic xxxxxxxxxxxxxxxxx where x is my base64 encoded auth string (myapi:TopSecret). My Token is in the body of the post

What am I missing? Why am I getting "MyAPI API uses invalid hashing algorithm"? If it's invalid, what is a valid hashing algorithm?

Additional information: My resources are contained in a SQL db accessed via Entity Framework. Specifically, the setup is just the same as in the quickstart documentation found here. To get to the point I'm at I had to manually add my API to the ApiSecrets table and give it a Type (SharedSecret) and a Value, that being a Sha256 password.

In Startup.cs my COnfigureServices includes

services.AddIdentityServer()
            .AddTemporarySigningCredential()
            .AddInMemoryApiResources(Configurations.Scopes.GetApiResources())
            .AddInMemoryClients(Configurations.Clients.GetClients())

            .AddConfigurationStore(builder =>
                builder.UseSqlServer(connectionString, options =>
                    options.MigrationsAssembly(migrationsAssembly)))
            .AddOperationalStore(builder =>
                builder.UseSqlServer(connectionString, options =>
                    options.MigrationsAssembly(migrationsAssembly)));

        // include the password validation routine
        services.AddTransient<IResourceOwnerPasswordValidator, Configurations.ResourceOwnerPasswordValidator>();
        services.AddTransient<IProfileService, Configurations.ProfileService>();

        services.AddMvc();

Under Configure:

app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
        {
            Authority = "http://localhost:5000",
            RequireHttpsMetadata = false,
            ApiSecret = "TopSecret",
            AutomaticAuthenticate = true,
            AutomaticChallenge = false,

            ApiName = "MyAPI"
        });

        InitializeDatabase(app);

        app.UseIdentityServer();

        app.UseMvc();

Note that I added the ApiSecret, AutomaticAuthenticate and AutomaticChallenge to this section only after I started having the problem in an effort to make it work.

In my Scopes.cs I have the following API outlined:

public static IEnumerable<ApiResource> GetApiResources()
    {
        return new[]
        {
            new ApiResource
            {
                Name = "MyAPI",
                DisplayName = "My API",
                ApiSecrets =
                {
                    new Secret("TopSecret".Sha256()),
                },

            }
        };            
    }

For Clients.cs:

public static IEnumerable<Client> GetClients()
    {
        return new List<Client>
    {

        new Client
        {
            ClientName = "My Client",
            AlwaysSendClientClaims=true,                
            ClientId = "MyClient",
            ClientSecrets = { new Secret("TopSecret".Sha256()) },
            RequireClientSecret=false,
            AllowAccessTokensViaBrowser =true,
            AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
            AllowedScopes = { "MyAPI" },
            RequireConsent = false,
            AllowOfflineAccess = true,

        },

That's more or less all there is to the code portion. The database that houses the configuration seems to be overriding any code changes I make though so I'm not sure how useful this all is. In the DB I created a record in the ApiSecrets table with an ApiResourceId of 1, added a description and expiration date, set the Type to "SharedSecret" and added the Secret using various formats including plain text, sha256 and base64.

Here's the full log during the call. Perhaps it will help. I see that there are some things about Bearer not being found or something like that but I'm not sure why that would be and if it affects the outcome of the procedure.

    info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 29.4277ms 401
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 POST http://localhost:5000/connect/introspect application/x-www-form-urlencoded 762
info: IdentityServer4.AccessTokenValidation.Infrastructure.NopAuthenticationMiddleware[7]
      Bearer was not authenticated. Failure message: No token found.
dbug: IdentityServer4.CorsPolicyProvider[0]
      CORS request made for path: /connect/introspect from origin: chrome-extension://aicmkgpgakddgnaphhhpliifpcfhicfo but rejected because invalid CORS path
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware[7]
      idsrv was not authenticated. Failure message: Unprotect ticket failed
dbug: IdentityServer4.Hosting.EndpointRouter[0]
      Request path /connect/introspect matched to endpoint type Introspection
dbug: IdentityServer4.Hosting.EndpointRouter[0]
      Mapping found for endpoint: Introspection, creating handler: IdentityServer4.Endpoints.IntrospectionEndpoint
info: IdentityServer4.Hosting.IdentityServerMiddleware[0]
      Invoking IdentityServer endpoint: IdentityServer4.Endpoints.IntrospectionEndpoint for /connect/introspect
dbug: IdentityServer4.Endpoints.IntrospectionEndpoint[0]
      Starting introspection request.
dbug: IdentityServer4.Validation.BasicAuthenticationSecretParser[0]
      Start parsing Basic Authentication secret
dbug: IdentityServer4.Validation.SecretParser[0]
      Parser found secret: BasicAuthenticationSecretParser
dbug: IdentityServer4.Validation.SecretParser[0]
      Secret id found: MyAPI
info: Microsoft.EntityFrameworkCore.Storage.IRelationalCommandBuilderFactory[1]
      Executed DbCommand (0ms) [Parameters=[@__name_0='?' (Size = 200)], CommandType='Text', CommandTimeout='30']
      SELECT TOP(1) [apiResource].[Id], [apiResource].[Description], [apiResource].[DisplayName], [apiResource].[Enabled], [apiResource].[Name]
      FROM [ApiResources] AS [apiResource]
      WHERE [apiResource].[Name] = @__name_0
      ORDER BY [apiResource].[Id]
info: Microsoft.EntityFrameworkCore.Storage.IRelationalCommandBuilderFactory[1]
      Executed DbCommand (0ms) [Parameters=[@__name_0='?' (Size = 200)], CommandType='Text', CommandTimeout='30']
      SELECT [a3].[Id], [a3].[ApiResourceId], [a3].[Type]
      FROM [ApiClaims] AS [a3]
      INNER JOIN (
          SELECT DISTINCT TOP(1) [apiResource].[Id]
          FROM [ApiResources] AS [apiResource]
          WHERE [apiResource].[Name] = @__name_0
          ORDER BY [apiResource].[Id]
      ) AS [apiResource2] ON [a3].[ApiResourceId] = [apiResource2].[Id]
      ORDER BY [apiResource2].[Id]
info: Microsoft.EntityFrameworkCore.Storage.IRelationalCommandBuilderFactory[1]
      Executed DbCommand (0ms) [Parameters=[@__name_0='?' (Size = 200)], CommandType='Text', CommandTimeout='30']
      SELECT [a2].[Id], [a2].[ApiResourceId], [a2].[Description], [a2].[Expiration], [a2].[Type], [a2].[Value]
      FROM [ApiSecrets] AS [a2]
      INNER JOIN (
          SELECT DISTINCT TOP(1) [apiResource].[Id]
          FROM [ApiResources] AS [apiResource]
          WHERE [apiResource].[Name] = @__name_0
          ORDER BY [apiResource].[Id]
      ) AS [apiResource1] ON [a2].[ApiResourceId] = [apiResource1].[Id]
      ORDER BY [apiResource1].[Id]
info: Microsoft.EntityFrameworkCore.Storage.IRelationalCommandBuilderFactory[1]
      Executed DbCommand (0ms) [Parameters=[@__name_0='?' (Size = 200)], CommandType='Text', CommandTimeout='30']
      SELECT [a].[Id], [a].[ApiResourceId], [a].[Description], [a].[DisplayName], [a].[Emphasize], [a].[Name], [a].[Required], [a].[ShowInDiscoveryDocument]
      FROM [ApiScopes] AS [a]
      INNER JOIN (
          SELECT DISTINCT TOP(1) [apiResource].[Id]
          FROM [ApiResources] AS [apiResource]
          WHERE [apiResource].[Name] = @__name_0
          ORDER BY [apiResource].[Id]
      ) AS [apiResource0] ON [a].[ApiResourceId] = [apiResource0].[Id]
      ORDER BY [apiResource0].[Id], [a].[Id]
info: Microsoft.EntityFrameworkCore.Storage.IRelationalCommandBuilderFactory[1]
      Executed DbCommand (0ms) [Parameters=[@__name_0='?' (Size = 200)], CommandType='Text', CommandTimeout='30']
      SELECT [a0].[Id], [a0].[ApiScopeId], [a0].[Type]
      FROM [ApiScopeClaims] AS [a0]
      INNER JOIN (
          SELECT DISTINCT [apiResource0].[Id], [a].[Id] AS [Id0]
          FROM [ApiScopes] AS [a]
          INNER JOIN (
              SELECT DISTINCT TOP(1) [apiResource].[Id]
              FROM [ApiResources] AS [apiResource]
              WHERE [apiResource].[Name] = @__name_0
              ORDER BY [apiResource].[Id]
          ) AS [apiResource0] ON [a].[ApiResourceId] = [apiResource0].[Id]
      ) AS [a1] ON [a0].[ApiScopeId] = [a1].[Id0]
      ORDER BY [a1].[Id], [a1].[Id0]
dbug: IdentityServer4.EntityFramework.Stores.ResourceStore[0]
      Found MyAPI API resource in database
info: IdentityServer4.Validation.HashedSharedSecretValidator[0]
      Secret: MyAPI Secret uses invalid hashing algorithm.
info: IdentityServer4.Validation.HashedSharedSecretValidator[0]
      Secret: MyAPI Secret uses invalid hashing algorithm.
dbug: IdentityServer4.Validation.SecretValidator[0]
      Secret validators could not validate secret
fail: IdentityServer4.Validation.ApiSecretValidator[0]
      API validation failed.
fail: IdentityServer4.Endpoints.IntrospectionEndpoint[0]
      API unauthorized to call introspection endpoint. aborting.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 30.673ms 401
like image 788
Rafe Avatar asked Feb 27 '17 20:02

Rafe


1 Answers

Without seeing every little detail of the code and database configuration it is a bit hard. Also, I don't see the code where you actually make the call to the Introspection endpoint. Are you doing that in C# or in Javascript or with Postman?

Anyways, here's my review...

Startup.cs

The ConfigureServices method looks good. Adding a ResourceOwner password validator service is unnecessary for the stated problem; to access the Introspection endpoint an ApiSecret is needed, not a ResourceOwner password. I assume you have it there for some unrelated reason, if not, then take it out.

Under the Configure method you have app.UseIdentityServerAuthentication which means you are using the web app to not only act as the authentication server (using IdentityServer4), but it is also your Web Api application that calls back to the authentication server (itself in this case) to validate incoming tokens.

app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
    Authority = "https://localhost:44388",
    RequireHttpsMetadata = false,
    ApiName = "MyAPI"
    //ApiSecret = "TopSecret" not necessary to know the api secret for normal validation
    //AutomaticAuthenticate = true, not necessary
    //AutomaticChallenge = false not necessary
});

You also might want to app.UseMvcWithDefaultRoute().

Api InMemory Configurations

Using the new ApiResource("name", "display") constructor will setup the database correctly; but using the object initializer syntax as you did above won't. This is an issue reported to GitHub: https://github.com/IdentityServer/IdentityServer4/issues/836

public static IEnumerable<ApiResource> GetApiResources()
{
    return new List<ApiResource>
    {
        // this will incorrectly leave out the ApiScope record in the database, but will create the ApiResoure and ApiSecret records
        new ApiResource
        {
            Name = "MyAPI",
            DisplayName = "My API",
            ApiSecrets =
            {
                new Secret("TopSecret".Sha256()),
            }
        },
        // this will correctly create the ApiResource, ApiScope, and ApiSecret records in the database.
        new ApiResource("MyAPI2", "My API2")
        {
            ApiSecrets =
            {
                new Secret("TopSecret2".Sha256())
            }
        }
    };
}

Fyi, since there are no Scopes specified in the new ApiResources directly above, the IdentityServer tooling will autogenerate one ApiScope for each ApiResource. The ApiScore gets the same name as the ApiResource in that case. An ApiResource must have at least one ApiScope; but could have many. It is the ApiScopes that are then related to the Client in ClientScopes table.

Client InMemory Configuration

See comments...

public static IEnumerable<Client> GetClients()
{
    return new List<Client>
    {
        new Client
        {
            ClientName = "My Client",
            AlwaysSendClientClaims = true,
            ClientId = "MyClient",
            // changed the secret to make clear this is unrelated to the Api secret
            ClientSecrets = { new Secret("TopSecretClientSecret".Sha256()) },
            // RequireClientSecret might as well be true if you are giving this client a secret
            RequireClientSecret = true,
            AllowAccessTokensViaBrowser = true,
            AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
            // Added MyAPI2 from my example above
            AllowedScopes = { "MyAPI", "MyAPI2" },
            RequireConsent = false,
            AllowOfflineAccess = true
        }
    };
}

Calling the Introspection Endpoint

The following code is from a WebApi controller. (Remember, the IdentityServer authority and ApiResource are hosted in the same web application in this discussion). A request to this method would be made by a Client.

Inside this method you can see it calls to its authority's introspection endpoint to validate/decrypt the access_token. This is not necessary in this example because we setup up the web application to app.UseIdentityServerAuthentication which already does this. An introspection endpoint would be used for Reference tokens or for when the web app is itself incapable of validating the access_token.

[Route("api/[controller]/[action]")]
[Produces("application/json")]
public class DataController : Controller
{
    [HttpGet]
    [Authorize]
    public async Task<IEnumerable<String>> Secure()
    {
        var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token");

        var introspectionClient = new IntrospectionClient("https://localhost:44388/connect/introspect", "MyAPI", "TopSecret");

        var response = await introspectionClient.SendAsync(new IntrospectionRequest { Token = accessToken });

        var isActive = response.IsActive;
        var claims = response.Claims;

        return new[] { "secure1", "secure2", $"isActive: {isActive}", JsonConvert.SerializeObject(claims) };
    }
}

Here, using an IntrospectionClient for the ApiScope "MyAPI" would give a 401 because the database is missing the ApiScope due to the object initializer problem previously mentioned.

One last thing

Another possible issue is that manually adding a hashed ApiSecret in the database editor could result in weird copy/paste issues making the text unable to be decrypted properly.

See full solution:

https://github.com/travisjs/AspNetCore-IdentityServer-Instrospection

I hope this can help get to the bottom of the issue or at least stimulate a new thought.

like image 62
travis.js Avatar answered Oct 21 '22 01:10

travis.js