ASP.NET Web Api not accepting own Openiddict JWT

Viewed 1497

I am learning OpenID Connect implementation in ASP.NET Core with a Web API project. My client is currently Postman.

Context (XY problem): I want Sendgrid to report Webhook data with authentication. Sendgrid uses OAuth 2 flow. I have mocked a Sendgrid Webhook invocation on Postman to use.

I followed a few tutorials to set up authorization server, ie. the part that will issue you a token, in particular using a temporary in-memory store based on EF Core. For the moment, this solution is sufficient to me and I'll have to do more researching and prototyping before becoming production-grade for reuse in future project.

I can successfully obtain a token with Postman using hardcoded credentials. Now I want the Controller APIs to validate tokens issued by the very same server. Let me show some code:

Startup.cs

   public void ConfigureServices(IServiceCollection services)
    {
        ....
        services.AddControllers();
        ....
  
        services.AddDbContext<OpenIddictDbContext>(ef => //OpenIddictDbContext contains no set, I want the framework to handle its own entities

             // Configure the context to use an in-memory store.
             ef.UseInMemoryDatabase(nameof(OpenIddictDbContext))

             // Register the entity sets needed by OpenIddict.
             .UseOpenIddict()
        );

        services.AddOpenIddict(options =>
            options.AddServer(server => server.AddEphemeralEncryptionKey()
                    .AddEphemeralSigningKey() //Not production-grade but I'll deal with this in the future
                    .RegisterScopes("api")
                    .SetTokenEndpointUris("/api/v1/Auth/token")
                    .SetAuthorizationEndpointUris("/api/v1/Auth/authorize")
                    .AllowClientCredentialsFlow()
                    .UseAspNetCore()
                    .EnableTokenEndpointPassthrough())
                .AddCore(core => core.UseEntityFrameworkCore()
                                  .UseDbContext<OpenIddictDbContext>())
                .AddValidation(validation => validation.UseLocalServer()))
            .AddHostedService<OpenIddictHostedService>() //Used only to create the tech-user for client, I prefer not to paste as it's mostly identical to linked tutorial
            .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(); //Security hardening (i.e. trusted authorities) TBD in the next future

}

AuthController.cs (identical except for the path)

    [HttpPost("token"), Produces("application/json")]
    public async Task<IActionResult> Token(CancellationToken cancellationToken = default)
    {
        var request = HttpContext.GetOpenIddictServerRequest();
        if (!request.IsClientCredentialsGrantType())
        {
            throw new NotImplementedException("The specified grant is not implemented.");
        }

        // Note: the client credentials are automatically validated by OpenIddict:
        // if client_id or client_secret are invalid, this action won't be invoked.

        var application =
            await _applicationManager.FindByClientIdAsync(request.ClientId, cancellationToken) ??
            throw new InvalidOperationException("The application cannot be found.");

        // Create a new ClaimsIdentity containing the claims that
        // will be used to create an id_token, a token or a code.
        var identity = new ClaimsIdentity(
            TokenValidationParameters.DefaultAuthenticationType,
            OpenIddictConstants.Claims.Name, OpenIddictConstants.Claims.Role);

        // Use the client_id as the subject identifier.
        identity.AddClaim(OpenIddictConstants.Claims.Subject,
            await _applicationManager.GetClientIdAsync(application, cancellationToken),
            OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken);

        identity.AddClaim(OpenIddictConstants.Claims.Name,
            await _applicationManager.GetDisplayNameAsync(application, cancellationToken),
            OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken);

        return SignIn(new ClaimsPrincipal(identity),
            OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    }

My code differs from code displayed in the tutorials because not all methods are found with OpenIddict 3.0, maybe the docs need some update.

At this point, I can get a token with Postman: take and eat; this is my ephemeral token

eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiJWMFZaS1pGR1c0WTJGUUpRNTNMQktaVktXSzZKS1pXV1lDMVREQUpDIiwidHlwIjoiYXQrand0In0.NNnSRN18drc3hj8PXon7bz7SyOpmhBds1fOQVnKZzD9dNSeFOQpc5MNJxkxcSSb3Z-XGGm-2z8Fw03yYJPBn2KkE9zz2udUpWNsWmToiiDOYd_5WVmm3GpZopWAusM-YhzPuavnxY1BFVsIylJs9yX0_jXs1RGIIhNvkGG_AmCBVnPdoDvu41CthRsyJc1IQCX5HuHO2XpvmnaRiVJZyaDbey4pDAs-4Fy-yDosg9w8szUWGcw-Y7e4xYKi6HqmjgDDqJQ14QDW839BOkctdRUrk3GhT7HDZN5Oaq8itPTLoxBrLgG9BE2yqfLKJibWD5MNSpj9OQu8GY-uBFsmt2A.YeBSpHgDu4291YJE-jWhOQ.WjxqjTXykAfFk7NRjXTMjPVsUFOtKK7fJxiLy0T7xiUnPSHIAI9m3UlkmsmQuK7sdxYtZnJ2gw-iJxcd1q1UYYRz5N82bc97py6bTH5cykWwMddCRMLnOFrOqKgTS8cZdgxfPwiz46SZKHYrxOtTtSU7Jy0YtNNYTBt47TXTMAjFwm81QrVmpPwEdt_wodaar7b23Wlw2SYbparw-hTdY-uaitL3olnCDzIn9Jwc7Zkqz8PwNxLYRPoUz1gpKJdO6c-azHcvYI8yY4Ty0OLdUuAZRGVwM4CRETW-Wixig37Sf6meXohJ1aS2Xe6LWAY5Db-Nyfs1H3D6tXL0hzI-q82xZO2oFNcj-yumYL14FoPlxl4TORgSh3L15rBY7e1VxzoJRPYuXAZUCOHrrdAF_DMnM7TSqe52ckMy2_aKNRsFaLZF8brwi4IDnXpe778Nw-ujTQ-djCPTZ-2gI1BP2h0NbpoSSKwI2_9eeUk_IsF5hq54En9oaQpNmz-7oPUAEa4GBmCeowrvzrtMKGMwPCsaOtYAJRnd63Y3YbTAe-NnGTH0EGvBga_9ElwDWswa-UR8CqeMLCqmDKOq9ryYbL4LWSHp7rmsykd2oHqzu_If8c5wzbLQBp3m-bt3w2EaYROXpLGlNvzVLla78Okwe9jFy8QeGVj1pCU8UsgwWCRzlwY4idV0SBSy8dPbEzCunLGbZgx2W81qXPLxWIybTGuOKyBvNffWXhnriUTjsL5khHrfArtjdbsoP93Ig0nqgOGiNXh82RVAuVVmLqOs6yohxLWUbDlxkiophBCmf8RD8h0Eak1zjmzQGTOS9D2BpWouZuEyZOHftnl07SmhXvZXLdw_gSZCoka4EY3FcIxb-9rw53wQAjn-00JLaMbIEaO79fZGJJytiNTagJJouVmvTCh-luNhJ2TULGC5nTb_szyY9yfvkfjK_tP7WIbW._42bMf01NOZ9tXyalK-ONdrPBDPqq-jL-TjWA2aHSuo

But every time I try to invoke a controller annotated with [Authorize] I get 401 with header WWW-Authenticate: Bearer error="invalid_token".

I believe that this might have to do with services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(), but trying to use OpenIddictServerAspNetCoreDefaults.AuthenticationScheme instead of JwtBearerDefaults.AuthenticationScheme results in the following error

The OpenIddict ASP.NET Core server handler cannot be used as the default scheme handler.
Make sure that neither DefaultAuthenticateScheme, DefaultChallengeScheme, DefaultForbidScheme, DefaultSignInScheme, DefaultSignOutScheme nor DefaultScheme point to an instance of the OpenIddict ASP.NET Core server handler.

Question

How to properly invoke an AuthorizeAttribute-protected method using a token issued by the very same server using Openiddict?

Note that I find no log about the authentication error. Raising log level properly can be a valid first step of investigation

Theory

From my knowledge of OIDC (my dissertation was on OAuth 2, so I can claim to be competent) and from perking around the code, here is what I am probably missing.

I have successfully implemented the authorization server part of OIDC cycle. Ie. with the code above I finally have an endpoint that validates client credentials and invokes SignIn. It is my understanding that ASP.NET Core's SignIn method assumes that calling code has already authenticated the user, by matching client ID and client secret.

It is my understanding that ASP.NET Core doesn't care anymore about the authentication, the SignIn releases a token that will be used later in the resource server cycle to authenticate further calls, as it is for Sendgrid to send Webhooks to me.

So in this case I should probably work on leveraging the authenticationScheme parameter correctly.

In a non-API scenario (or if the resource consumer ever supported that) I could use a cookie, so that SignIn will release a cookie to the HttpResponse object.

Or in this case I could either find the correct way to integrate the resource server flow of Openiddict, or leverage a different authenticator like ASP.NET Core's embedded JWT.

For the latter, SignIn could just act as a delegator. "Hey, Mr. JWT Provider, I successfully authenticated Sendrig here who's knocking at our door calling our authentication API. Would you mind issuing a valid token that you will accept in the future?".

I was unsuccessful at using

 return SignIn(new ClaimsPrincipal(identity),
           // OpenIddictServerAspNetCoreDefaults.AuthenticationScheme
           JwtBearerDefaults.AuthenticationScheme
            );

But the above code should tell JWT Provider to release a token for the claimed identity.

1 Answers

The key was to add the correct authentication scheme

services
.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)

From example

Full code fragment

        services.AddOpenIddict(options =>
                options.AddServer(server => server.AddEphemeralEncryptionKey()
                        .AddEphemeralSigningKey()
                        .RegisterScopes("api")
                        .SetTokenEndpointUris("/api/v1/Auth/token")
                        .SetAuthorizationEndpointUris("/api/v1/Auth/authorize")
                        .AllowClientCredentialsFlow()
                        .UseAspNetCore()
                        .EnableTokenEndpointPassthrough())
                    .AddCore(core => core.UseEntityFrameworkCore()
                        .UseDbContext<OpenIddictDbContext>())
                    .AddValidation(validation =>
                        validation.UseLocalServer(_ => { })
                            .UseAspNetCore(_ => { })
                    )
            )
            .AddHostedService<OpenIddictHostedService>()
            .AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)
            ;

This also required me to add Openiddict.Validation.AspNetCore as project dependency

Related