Implementing API Key Authentication in ASP.NET Core

June 23, 2024 by Anuraj

AspNetCore DotNet

In this blog post, we’ll learn how to implement API Key Authentication in ASP.NET Core. When exposing API endpoints to other developers or consumers it is a best practice to implement authentication system. Api Key authentication is very easy to implement and configure. In this approach, developers need to provide the API Key along with the request details.

Creating ASP.NET Core Web API

We can implement the authentication mechanism in various ways, but for this blog post we are implementing it using AuthenticationHandler class. First we will create an ASP.NET Core Web API project - we can create with controllers or Minimal API style - it works in both styles. We can create the Web API project using the command dotnet new webapi --name WeatherForecast.Api --output WeatherForecast.Api\Src\WeatherForecast.Api --use-controllers - this command will create a web api application, the --use-controllers parameter will create the project with controllers, by default the web api project will be created with Minimal API.

Implementing the Authentication Configuration option

After creating the project, we need to create ApiKeyAuthenticationSchemeOptions which inherits from AuthenticationSchemeOptions class - this class will use to configure the authentication options.

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

In the above code, we are creating an ApiKey property which we will be reading from the configuration. And we will be comparing it with the value we receive from the Authentication header.

Implementing the Authentication Handler

Next we need to implement ApiKeyAuthenticationSchemeHandler which will be inheriting from AuthenticationHandler class - as the name implies this class will handle the authentication requests. Here is the implementation.

public class ApiKeyAuthenticationSchemeHandler : AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>
{
    public ApiKeyAuthenticationSchemeHandler(IOptionsMonitor<ApiKeyAuthenticationSchemeOptions> options, ILoggerFactory logger, 
        UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue("X-Api-Key", out var apiKeyHeaderValues))
        {
            return Task.FromResult(AuthenticateResult.Fail("Missing API Key"));
        }

        var apiKey = apiKeyHeaderValues.FirstOrDefault();
        if (string.IsNullOrEmpty(apiKey))
        {
            return Task.FromResult(AuthenticateResult.Fail("Missing API Key"));
        }

        if (apiKey != Options.ApiKey)
        {
            return Task.FromResult(AuthenticateResult.Fail("Invalid API Key"));
        }

        var claims = new[] { new Claim(ClaimTypes.Name, "API 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));
    }
}

We will get some warnings once we implement this class - in .NET 8.0 the ISystemClock interface is deprecated. We can remove this from the parameter and this warning will be removed. Here is the updated implementation.

public class ApiKeyAuthenticationSchemeHandler : AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>
{
    public ApiKeyAuthenticationSchemeHandler(IOptionsMonitor<ApiKeyAuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue("X-Api-Key", out var apiKeyHeaderValues))
        {
            return Task.FromResult(AuthenticateResult.Fail("Missing API Key"));
        }

        var apiKey = apiKeyHeaderValues.FirstOrDefault();
        if (string.IsNullOrEmpty(apiKey))
        {
            return Task.FromResult(AuthenticateResult.Fail("Missing API Key"));
        }

        if (apiKey != Options.ApiKey)
        {
            return Task.FromResult(AuthenticateResult.Fail("Invalid API Key"));
        }

        var claims = new[] { new Claim(ClaimTypes.Name, "API 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));
    }
}

In this implementation, first we will be trying to get X-Api-Key header value, if it is not available, we will return an authentication failure error. Next we will be validating whether the value is empty or not. And finally we will verify the value we received as part of the request is same as the one we read from configuration. If all the conditions are satisfied, we will be creating a identity and returns it.

Configuring the Api Key authentication

We can associate the ApiKey authentication in our Web API like this.

builder.Services.AddAuthentication("ApiKey")
    .AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationSchemeHandler>("ApiKey", 
    options => { options.ApiKey = builder.Configuration["ApiKey"]; });

We also need to add the ApiKey in the appsettings.json file.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ApiKey": "1234567890"
}

We have completed the configuration, next we need to attribute the controllers or methods with [Authorize] attribute to protect them from anonymous access.

[Authorize]
public class WeatherForecastController : ControllerBase
{
}

Now we can call the /WeatherForecast endpoint without X-Api-Key header and which will return not authorized (401) error. Next we can send the request with X-Api-Key header and we will get the proper response.

Configure Swagger UI to support Api Key authentication

For support Api Key authentication, we need to add SecurityDefinition in Swagger. Here is the implementation.

builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
    {
        Description = "X-API-KEY must appear in header",
        Type = SecuritySchemeType.ApiKey,
        Name = "X-API-KEY",
        In = ParameterLocation.Header,
        Scheme = "ApiKeyScheme"
    });

    var key = new OpenApiSecurityScheme()
    {
        Reference = new OpenApiReference
        {
            Type = ReferenceType.SecurityScheme,
            Id = "ApiKey"
        },
        In = ParameterLocation.Header
    };
    var requirement = new OpenApiSecurityRequirement { { key, new List<string>() } };
    options.AddSecurityRequirement(requirement);
});

Now run the application, we will be able to see the Authorize button. And clicking on it will open a dialog like this

Swagger Authorize UI

Where we can enter the API Key and help us call the API endpoints.

The implementation will work with Minimal APIs as well. For Minimal APIs instead of Authorize attribute we need to use RequireAuthorization method.

app.MapGet("/weatherforecast-minimal", () =>
{
    string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    return Enumerable.Range(1, 5).Select(index => new WeatherForecast.Api.WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    }).ToArray();
}).RequireAuthorization();

In this blog post, we learned about implementing Api Key Authentication and how to use it with Swagger UI. The source code for this blog post available in GitHub

Happy Programming

Copyright © 2024 Anuraj. Blog content licensed under the Creative Commons CC BY 2.5 | Unless otherwise stated or granted, code samples licensed under the MIT license. This is a personal blog. The opinions expressed here represent my own and not those of my employer. Powered by Jekyll. Hosted with ❤ by GitHub