create an API with .NET Minimal APIs that require session api key

Viewed 1944

This video is really nice and shows how to create Minimal APIs using .net 6:

https://www.youtube.com/watch?v=eRJFNGIsJEo

It is amazing how it uses dependency injection to get mostly everything that you need inside your endpoints. For example if I need the value of a custom header I would have this:

app.MapGet("/get-custom-header", ([FromHeader(Name = "User-Agent")] string data) =>
{
    return $"User again is: {data}";
});

I can have another endpoint where I have access to the entire httpContext like this:

app.MapGet("/foo", (Microsoft.AspNetCore.Http.HttpContext c) =>
{
    var path = c.Request.Path;
    return path;
});

I can even register my own classes with this code: builder.Services.AddTransient<TheClassIWantToRegister>()

If I register my custom classes I will be able to create an instance of that class every time I need it on and endpoint (app.MapGet("...)


Anyways back to the question. When a user logs in I send him this:

{
  "ApiKey": "1234",
  "ExpirationDate": blabla bla
  .....
}

The user must send the 1234 token to use the API. How can I avoid repeating my code like this:

app.MapGet("/getCustomers", ([FromHeader(Name = "API-KEY")] string apiToken) =>
{
    // validate apiToken agains DB
    if(validationPasses)
       return Database.Customers.ToList();
    else
       // return unauthorized
});

I have tried creating a custom class RequiresApiTokenKey and registering that class as builder.Services.AddTransient<RequiresApiTokenKey>() so that my API knows how to create an instance of that class when needed but how can I access the current http context inside that class for example? How can I avoid having to repeat having to check if the header API-KEY header is valid in every method that requires it?

2 Answers

Gave this a test based on my comments.

This would call the method Invoke in the middleware on each request and you can do checks here.

Probably a better way would be to use the AuthenticationHandler. using this would mean you can attribute individual endpoints to have the API key check done instead of all incoming requests

But, I thought this was still useful, middleware can be used for anything you'd like to perform on every request

Using Middleware

Program.cs:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

//our custom middleware extension to call UseMiddleware
app.UseAPIKeyCheckMiddleware();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapGet("/", () => "Hello World!");

app.Run();

APIKeyCheckMiddleware.cs

using Microsoft.Extensions.Primitives;

internal class APIKeyCheckMiddleware
{
    private readonly RequestDelegate _next;

    public APIKeyCheckMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        
        //we could inject here our database context to do checks against the db
        if (httpContext.Request.Headers.TryGetValue("API-KEY", out StringValues value))
        {
            //do the checks on key
            var apikey = value;
        }
        else
        {
            //return 403
            httpContext.Response.StatusCode = 403;
        }
        
        await _next(httpContext);
    }
}

// Extension method used to add the middleware to the HTTP request pipeline.
public static class APIKeyCheckMiddlewareExtensions
{
    public static IApplicationBuilder UseAPIKeyCheckMiddleware(this IApplicationBuilder builder)
    {
        
        return builder.UseMiddleware<APIKeyCheckMiddleware>();
    }
}

I used SmithMart's answer but had to change things in the Invoke method and used DI in the constructor. Here's my version:

internal class ApiKeyCheckMiddleware
    {
        public static string ApiKeyHeaderName = "X-ApiKey";
        private readonly RequestDelegate _next;
        private readonly ILogger<ApiKeyCheckMiddleware> _logger;
        private readonly IApiKeyService _apiKeyService;

        public ApiKeyCheckMiddleware(RequestDelegate next, ILogger<ApiKeyCheckMiddleware> logger, IApiKeyService apiKeyService)
        {
            _next = next;
            _logger = logger;
            _apiKeyService = apiKeyService;
        }

        public async Task InvokeAsync(HttpContext httpContext)
        {
            var request = httpContext.Request;
            var hasApiKeyHeader = request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyValue);

            if (hasApiKeyHeader)
            {
                _logger.LogDebug("Found the header {ApiKeyHeader}. Starting API Key validation", ApiKeyHeaderName);

                if (apiKeyValue.Count != 0 && !string.IsNullOrWhiteSpace(apiKeyValue))
                {
                    if (Guid.TryParse(apiKeyValue, out Guid apiKey))
                    {
                        var allowed = await _apiKeyService.Validate(apiKey);

                        if (allowed)
                        {
                            _logger.LogDebug("Client successfully logged in with key {ApiKey}", apiKeyValue);

                            var apiKeyClaim = new Claim("ApiKey", apiKeyValue);
                            var allowedSiteIdsClaim = new Claim("SiteIds", string.Join(",", allowedSiteIds));
                            var principal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> { apiKeyClaim, allowedSiteIdsClaim }, "ApiKey"));
                            httpContext.User = principal;

                            await _next(httpContext);

                            return;
                        }
                    }

                    _logger.LogWarning("Client with ApiKey {ApiKey} is not authorized", apiKeyValue);
                }
                else
                {
                    _logger.LogWarning("{HeaderName} header found, but api key was null or empty", ApiKeyHeaderName);
                }
            }
            else
            {
                _logger.LogWarning("No ApiKey header found.");
            }

            httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
        }
    }
Related