18

I am attempting to build a super simple API-key authentication for certain APIs in a controller. For this I have this in ConfigureServices():

services.AddAuthorization(options =>
{
  options.AddPolicy(
    Auth.Constants.WebmasterPolicyName,
    policy =>
      policy.RequireAssertion(context =>
      {
        if (context.Resource is HttpContext httpContext)
        {
          if (httpContext.Request.Headers.TryGetValue("X-API-KEY", out var header))
          {
            var val = header.FirstOrDefault()?.ToLower();
            if (val == "my-super-secret-key")
            {
              return Task.FromResult(true);
            }
          }
        }
        return Task.FromResult(false);
      })
  );
});

I have decorated an API with this:

[HttpDelete("{itemId:guid}")]
[Authorize(Policy = Auth.Constants.WebmasterPolicyName)]
public async Task<ActionResult> DeleteCatalogItemAsync(Guid itemId)

This works perfectly, when I set the correct API key in the request.

The problem is the negative case: When the key is missing or wrong, I will get a 500 error:

System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).
   at Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext context, String scheme, AuthenticationProperties properties)
   at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at AlwaysOn.CatalogService.Startup.<>c__DisplayClass5_0.<<Configure>b__3>d.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)

But I'm not sure what to do with that message. I would just like it to return a 401 response to the client.

12
  • 2
    What order is app.UseAuthentication() and app.UseAuthorization() in Configure()? I suspect that by you explicitly authorising, it is not calling the authentication handler but if auth fails, it calls the authentication handler which is not setup to handle any schemes. It is a very confusing area! Commented Dec 8, 2021 at 15:37
  • 1
    @LukeBriner right now I dont have any AddAuthentication() in there - since I had no idea what to put in it?! Commented Dec 8, 2021 at 15:48
  • 1
    API Key Authentication should not be implemented this way. Please check out this blog post for help. Commented Dec 9, 2021 at 3:16
  • 1
    @JasonPan Could you elaborate why api key authentication should not be implemented this way? Using a custom middleware or action filter attribute does not play nicely with the asp net authorization. Let's say you have set a Fallback policy, it will also execute, which likely is not desirable. Commented Aug 18, 2022 at 13:16
  • 1
    To continue on my comment above, I understand that we're actually talking about authentication here and only implementing an authorization policy alone isn't enough. I was thinking that implementing a custom authentication scheme in combination with a custom authorization policy might be the correct way to do this, which led me to this question. I'm facing a problem where we have an api that can be accessed with different authentication methods (incl api key) and want to have a fallback policy where the user needs to be authenticated. Commented Aug 18, 2022 at 13:44

2 Answers 2

48

You need to implement a custom authentication handler in order to properly handle this. Here is a good example on how to do it.

To summarize the idea, you need to register a custom authentication scheme:

builder.Services.AddAuthentication("ApiKey")
    .AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationSchemeHandler>(
        "ApiKey",
        opts => opts.ApiKey = configuration.GetValue<string>("api-key")
    );

And define a class for options:

public class ApiKeyAuthenticationSchemeOptions: AuthenticationSchemeOptions {
  public string ApiKey {get; set;}
}

And implement a handler class ApiKeyAuthenticationSchemeHandler:AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>. With a method

protected override Task<AuthenticateResult> HandleAuthenticateAsync() {
   var apiKey = Context.Request.Headers["X-API-KEY"];
   if (apiKey != Options.ApiKey) {
       return Task.FromResult(AuthenticateResult.Fail("Invalid X-API-KEY"));
   }
   var claims = new[] { new Claim(ClaimTypes.Name, "VALID USER") };
   var identity = new ClaimsIdentity(claims, Scheme.Name);
   var principal = new ClaimsPrincipal(identity);
   var ticket = new AuthenticationTicket(principal, Scheme.Name);
   return Task.FromResult(AuthenticateResult.Success(ticket));
} 

And last, in your controller you can add an Authorize attibute, like this:

[Authorize(AuthenticationSchemes = "ApiKey")]

Sign up to request clarification or add additional context in comments.

4 Comments

Great answer +1, but I have a question. What does VALID USER mean here? API Key auth happens for machine-machine communication, doesn't it? So what does a User mean here? Can you please clarify? Also how do I use this in a Minimal API? Thanks!
Also the message "Invalid X-API-KEY" doesn't show up on the response, just says 401. I tested using Swagger UI. Take a look at it if you'd like. Full source code here.
@AshK VALID USER here is just an arbitrary name. However, if an application has multiple API keys, each key could have a name representing the calling system, so it could be used to distinguish requests made with different keys in the logs etc. Regarding 401 - I don't think it will be exposed in the response automatically, a custom handling is needed for that.
Oh ok gotcha. That makes sense. "Regarding 401 - I don't think it will be exposed in the response automatically, a custom handling is needed for that." Can you point me to some example of that custom handling? I was able to show message in the response using Endpoint filter but that's not true Auth as I can't create claims principal that way.
16

We can create a custom ApiKeyMiddleware to implemente simple API key authentication.

It is somehow similar to what we have done in the custom attribute, but the main difference that you will notice here is that we cannot directly set the Response object of the context but we have to assign the statuscode and message separately.

Sample Code:

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;

namespace SecuringWebApiUsingApiKey.Middleware
{
public class ApiKeyMiddleware
{
    private readonly RequestDelegate _next;
    private const string APIKEYNAME = "ApiKey";
    public ApiKeyMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        if (!context.Request.Headers.TryGetValue(APIKEYNAME, out var extractedApiKey))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsync("Api Key was not provided. (Using ApiKeyMiddleware) ");
            return;
        }

        var appSettings = context.RequestServices.GetRequiredService<IConfiguration>();

        var apiKey = appSettings.GetValue<string>(APIKEYNAME);

        if (!apiKey.Equals(extractedApiKey))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsync("Unauthorized client. (Using ApiKeyMiddleware)");
            return;
        }

        await _next(context);
    }
}
}

For more details, we can refer this blog.

Secure ASP.NET Core Web API using API Key Authentication

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.