Implementing API Key Validation with Middleware in ASP.NET Core

In modern API development, securing endpoints is crucial. While some advocate using API key filters for this purpose, middleware provides a more streamlined and centralized solution. Middleware operates early in the request pipeline, allowing for robust and flexible authentication handling before requests reach the controllers.

Why Use Middleware for API Key Validation?

Middleware offers several advantages over filters. It provides a centralized location for handling cross-cutting concerns like authentication, logging, and error handling. This approach simplifies the codebase by keeping authentication logic separate from business logic, enhancing maintainability and readability. Additionally, middleware can handle requests more efficiently, reducing the overhead associated with attribute-based validation mechanisms.

How to Implement API Key Validation Middleware

To implement API key validation using middleware, we create a custom middleware class. This class will intercept incoming HTTP requests, check for a valid API key, and either allow the request to proceed or return an appropriate error response.

public class ApiKeyAndErrorHandlerMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ApiKeyService _apiKeyService;
        private const string APIKEY = "X-API-KEY";


        public ApiKeyAndErrorHandlerMiddleware(RequestDelegate next, ApiKeyService apiKeyService)
        {
            _next = next;
            _apiKeyService = apiKeyService;
        }

        public async Task Invoke(HttpContext context)
        {
            try
            {
                // API Key Validation
                if (!context.Request.Headers.TryGetValue(APIKEY, out var extractedApiKey))
                {
                    await HandleError(context, "API Key was not provided", HttpStatusCode.Forbidden);
                    return;
                }

                var result = await _apiKeyService.GetSingleWithWhereAsync(Guid.Parse(extractedApiKey));
                if (result == null)
                {
                    await HandleError(context, "Unauthorized Client", HttpStatusCode.Forbidden);
                    return;
                }

                // Call the next middleware in the pipeline
                await _next(context);
            }
            catch (Exception error)
            {
                await HandleException(context, error);
            }
        }
}

Middleware Design and Functionality

  1. API Key Validation:

    • Purpose: To ensure that only clients with valid API keys can access the API endpoints.
    • Implementation: The middleware checks for the presence of an X-API-KEY header. If the header is missing or the key is invalid, it responds with a 403 Forbidden status and a corresponding message.
    • Example:
       
      if (!context.Request.Headers.TryGetValue(APIKEY, out var extractedApiKey)) { await HandleError(context, "API Key was not provided", HttpStatusCode.Forbidden); return; }
  2. Error Handling:

    • Purpose: To handle exceptions gracefully and provide meaningful error responses.
    • Implementation: The middleware catches exceptions and classifies them into different categories (ApiException, KeyNotFoundException, and general exceptions). It then sets the appropriate HTTP status code and logs the error using Serilog.
    • Example:
       
      catch (Exception error) { await HandleException(context, error); }
  3. Logging with Serilog:

    • Purpose: To log error details for monitoring and debugging purposes.
    • Implementation: The middleware uses Serilog to log error details. This helps in diagnosing issues by providing a detailed error message and stack trace.
    • Example:

      Serilog.Log.Error(error, "An error occurred: {Message}", error.Message);

Program.cs Pipeline Order

The order in which middleware components are added to the pipeline is crucial for the correct functioning of the application. Here’s a detailed explanation of the order and rationale:

  1. UseDeveloperExceptionPage:

    • Purpose: Provides detailed exception information during development.
    • Placement: Should be at the top when in development mode to catch and display exceptions early in the pipeline.
  2. UseSwagger:

    • Purpose: Enables Swagger for API documentation.
    • Placement: Placed early to ensure that Swagger UI is accessible.
  3. UseHttpsRedirection:

    • Purpose: Redirects HTTP requests to HTTPS.
    • Placement: Early in the pipeline to ensure all subsequent middleware handles requests securely.
  4. Custom Middleware (ApiKeyAndErrorHandlerMiddleware):

    • Purpose: Handles API key validation and error handling.
    • Placement: Placed after HTTPS redirection to validate requests and handle errors before processing further.
  5. UseCors:

    • Purpose: Enables CORS to allow requests from specified origins.
    • Placement: Placed early to ensure all requests have the necessary CORS headers.
  6. UseRouting:

    • Purpose: Sets up the routing for the application.
    • Placement: Needs to be before authentication and authorization to set up request routing.
  7. UseAuthentication:

    • Purpose: Handles user authentication.
    • Placement: Placed before authorization to ensure the user is authenticated.
  8. UseAuthorization (if needed):

    • Purpose: Handles user authorization.
    • Placement: After authentication to ensure the user is authorized to access resources.
  9. UseResponseCompression:

    • Purpose: Compresses responses to reduce payload size.
    • Placement: Placed late in the pipeline to ensure all responses are compressed.
  10. MapControllers and MapHealthChecks:

    • Purpose: Maps controllers and health checks to their respective endpoints.
    • Placement: At the end to ensure all middleware is applied before reaching the endpoints.

Final Thoughts

The refactored middleware consolidates error handling and API key validation into a single, efficient component, simplifying the pipeline. Proper logging with Serilog ensures that any issues can be tracked and diagnosed effectively.