Search code examples
c#asp.net-coreasp.net-core-webapiasp.net-authorization

How to protect every API endpoints by API key in header 'x-api-key' in .NET


I wrote an authorization middleware for API key in ASP.NET Core. My goal is to secure all API endpoints so that they require a valid API key in the 'x-api-key' header. My problem is, that when I am not sure how to use it right.

I don't know if I am missing something or should I add more code. The token is empty (returns 401 code).

Example of using it:

[HttpGet]
[ApiKeyAuthorization]
[Route("subgroups/{parentId}")]
public IActionResult GetSubgroups(int parentId)
{
    // some code...
}

The code behind:

public class ApiKeyMiddleware
{
        private readonly RequestDelegate _next;
        private readonly IConfiguration _configuration;

        public ApiKeyMiddleware(RequestDelegate next, IConfiguration configuration)
        {
            _next = next;
            _configuration = configuration;
        }

        public async Task Invoke(HttpContext context)
        {
            var apiKey = context.Request.Headers["x-api-key"].FirstOrDefault();

            if (!string.IsNullOrEmpty(apiKey) && _configuration["AllowedApiKeys"]!.Contains(apiKey))
            {
                await _next(context);
            }
            else
            {
                context.Response.StatusCode = 401; // Unauthorized
                await context.Response.WriteAsync("Unauthorized");
            }
        }
    }

    [AttributeUsage(AttributeTargets.Method)]
    public class ApiKeyAuthorizationAttribute : Attribute, IAuthorizationFilter
    {
        private readonly IConfiguration? _configuration;

        public void OnAuthorization(AuthorizationFilterContext context)
        {
            var apiKey = context.HttpContext.Request.Headers["x-api-key"].FirstOrDefault();

            if (string.IsNullOrEmpty(apiKey) || !IsApiKeyValid(apiKey))
            {
                context.Result = new UnauthorizedResult();
            }
        }

        private bool IsApiKeyValid(string apiKey)
        {
            var allowedApiKeys = _configuration.GetSection("AllowedApiKeys").Get<string[]>();
            return allowedApiKeys.Contains(apiKey);
        }
    }

Solution

  • Please follow my steps to implement this function.

    My Project Structure

    enter image description here

    ApiKeyMiddleware.cs

    using Microsoft.AspNetCore.Mvc.Filters;
    using Microsoft.AspNetCore.Mvc;
    
    namespace WebApplication5
    {
        public class ApiKeyMiddleware
        {
            private readonly RequestDelegate _next;
            private readonly IConfiguration _configuration;
    
            public ApiKeyMiddleware(RequestDelegate next, IConfiguration configuration)
            {
                _next = next;
                _configuration = configuration;
            }
    
            public async Task Invoke(HttpContext context)
            {
                var apiKey = context.Request.Headers["x-api-key"].FirstOrDefault();
    
                if (!string.IsNullOrEmpty(apiKey) && _configuration.GetSection("AllowedApiKeys").Get<string[]>()!.Contains(apiKey))
                {
                    await _next(context);
                }
                else
                {
                    context.Response.StatusCode = 401; // Unauthorized
                    await context.Response.WriteAsync("Unauthorized");
                }
            }
        }
        // Extension method used to add the middleware to the HTTP request pipeline.
        public static class ApiKeyMiddlewareExtensions
        {
            public static IApplicationBuilder UseApiKeyMiddleware(this IApplicationBuilder builder)
            {
                return builder.UseMiddleware<ApiKeyMiddleware>();
            }
        }
    }
    

    ApiKeyAuthenticationHandler.cs

    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.DataProtection.KeyManagement;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.Options;
    using System.Security.Claims;
    using System.Text.Encodings.Web;
    
    namespace WebApplication5
    {
        public class ApiKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions
        {
            public string? ApiKey { get; set; }
        }
        public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>
        {
            private readonly IConfiguration _configuration;
            //TODO Change to whatever name you want to use
            private const string ApiKeyHeaderName = "x-api-key";
    
            public ApiKeyAuthenticationHandler(
               IOptionsMonitor<ApiKeyAuthenticationSchemeOptions> options,
               ILoggerFactory logger,
               UrlEncoder encoder,
               ISystemClock clock,
               IConfiguration configuration)
               : base(options, logger, encoder, clock)
            {
                _configuration = configuration;
            }
    
            protected override Task<AuthenticateResult> HandleAuthenticateAsync()
            {
                if (!Request.Headers.ContainsKey(ApiKeyHeaderName))
                {
                    return Task.FromResult(AuthenticateResult.Fail("Header was not found"));
                }
    
                string token = Request.Headers[ApiKeyHeaderName].ToString();
    
    
                if (string.IsNullOrEmpty(token) || !IsApiKeyValid(token))
                {
                    return Task.FromResult(AuthenticateResult.Fail("Token is invalid"));
                }
                else {
                    Claim[] claims = new[] {
                        new Claim(ClaimTypes.NameIdentifier, "jason p"),
                        new Claim(ClaimTypes.Email, "jasonp***@gmail.com"),
                    };
    
                    ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, nameof(ApiKeyAuthenticationHandler));
                    AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name);
    
                    return Task.FromResult(AuthenticateResult.Success(ticket));
                }
                
            }
            private bool IsApiKeyValid(string apiKey)
            {
                var allowedApiKeys = _configuration.GetSection("AllowedApiKeys").Get<string[]>();
                return allowedApiKeys.Contains(apiKey);
            }
        }
    }
    

    Add AddAuthentication and use the apikey middleware in Program.cs file.

    namespace WebApplication5
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                var builder = WebApplication.CreateBuilder(args);
    
                // Add services to the container.
                builder.Services.AddAuthentication("ApiKey").AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>("ApiKey",opts => opts.ApiKey = builder.Configuration.GetValue<string>("AllowedApiKeys")
                );
                builder.Services.AddControllers();
                // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
                builder.Services.AddEndpointsApiExplorer();
                builder.Services.AddSwaggerGen();
    
                var app = builder.Build();
    
                // Configure the HTTP request pipeline.
                if (app.Environment.IsDevelopment())
                {
                    app.UseSwagger();
                    app.UseSwaggerUI();
                }
    
                app.UseHttpsRedirection();
    
                app.UseApiKeyMiddleware();
    
                app.UseAuthorization();
    
    
                app.MapControllers();
    
                app.Run();
            }
        }
    }
    

    My appsettings.json

    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*",
      "AllowedApiKeys": ["aaa","bbb","ccc"]
    }
    

    And we can use [Authorize(AuthenticationSchemes = "ApiKey")] to protect the api controller.

    enter image description here

    Test Result

    Use the wrong key aa first, then use the correct one aaa later to check it.

    enter image description here