Peter_Griffindor
Peter_Griffindor

Reputation: 85

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);
        }
    }

Upvotes: 1

Views: 2290

Answers (1)

Jason Pan
Jason Pan

Reputation: 21883

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

Upvotes: 2

Related Questions