DEV Community

Cover image for API Key Authentication - Extending the native implementation
Ricardo
Ricardo

Posted on • Updated on • Originally published at rmauro.dev

API Key Authentication - Extending the native implementation

In this article we're going to create the code (and understand how it works) for handle API Key authentication with just three lines of code extending the native Authentication mechanism.

services.AddAuthentication(ApiKeyAuthNDefaults.SchemaName)
    .AddApiKey(opt => //here is our handler
    {
        opt.ApiKey = "Hello-World";
        opt.QueryStringKey = "key";
    });
Enter fullscreen mode Exit fullscreen mode
Solution - Adding API Key Authentication Service

We want the simple and stupid solution and not some crazy implementation using MVC [Attributes] or any customized middleware to handle the Authentication.

Ok, ok, ok. I know it's hard to find a good implementation of API Key Authentication out there on the internet. I think it's also hard to ourselfs needing of API Key Authentication on daily basis.
But now you found it now! Hope you like it. Leave a comment :)

Disclaimer: Maybe I'm writing this article mad with someone hahahahaha. Please forgive me.

Introduction

The native implementation of ASP.NET Authentication allow us to extend it and create our own validation logic.
With the AddScheme builder we're going to implement the APIKey Authentication.

Everythin begins with the services.AddAuthentication code. This builder provide us the ability to use the method AddScheme. Here is where our Auth ApiKey handler goes.

Starting with the Code

Let's start by creating the file ApiKeyAuthNOptions.cs. This file will contain all configuration of our ApiKeyAuthN service, such as the QueryStringKey and ApiKey.

using Microsoft.AspNetCore.Authentication;

namespace APIAuthentication.Resource.Infrastructure
{
    public class ApiKeyAuthNOptions : AuthenticationSchemeOptions
    {
        public string ApiKey { get; set; }

        public string QueryStringKey { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode
ApiKeyAuthNOptions.cs

Second step is the file ApiKeyAuthN.cs with the following content.

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace APIAuthentication.Resource.Infrastructure
{
    public static class ApiKeyAuthNDefaults
    {
        public const string SchemaName = "ApiKey";
    }

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

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            throw new System.NotImplementedException();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
Initial implementation of ApiKeyAuthN.cs

The class AuthenticationHandler is responsable to making the validation and create the Authentication Ticket for the user.

I think you can guess where to put the validation logic, right?
Here is the implementation.

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
    var apiKey = ParseApiKey(); // handles parsing QueryString

    if (string.IsNullOrEmpty(apiKey)) //no key was provided - return NoResult
        return Task.FromResult(AuthenticateResult.NoResult());

    if (string.Compare(apiKey, Options.ApiKey, StringComparison.Ordinal) == 0)
    {
        var principal = BuildPrincipal(Scheme.Name, Options.ApiKey, Options.ClaimsIssuer ?? "ApiKey");
        return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name))); //Success. Key matched
    }

    return Task.FromResult(AuthenticateResult.Fail($"Invalid API Key provided.")); //Wrong key was provided
}
Enter fullscreen mode Exit fullscreen mode
HandleAuthentication - ApiKeyAuthN.cs
protected string ParseApiKey()
{   
    if (Request.Query.TryGetValue(Options.QueryStringKey, out var value))
        return value.FirstOrDefault();

    return string.Empty;
}
Enter fullscreen mode Exit fullscreen mode
ParseApiKey method - ApiKeyAuthN.cs
static ClaimsPrincipal BuildPrincipal(string schemeName, string name, string issuer, params Claim[] claims)
{
    var identity = new ClaimsIdentity(schemeName);

    identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, name, ClaimValueTypes.String, issuer));
    identity.AddClaim(new Claim(ClaimTypes.Name, name, ClaimValueTypes.String, issuer));

    identity.AddClaims(claims);

    var principal = new ClaimsPrincipal(identity);
    return principal;
}
Enter fullscreen mode Exit fullscreen mode
BuildPrincipal method - ApiKeyAuthN.cs

The implentation of BuildPrincipal is up to you. You should customize the ClaimsIdentity with the Claims you find necessary in your application, such as Role, PhoneNumber, Issuer, Partner Id, among others.

Wraping thing up - We're almost there

We have everything we need to start the authentication. Open your Startup.cs file and add the following contents.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(ApiKeyAuthNDefaults.SchemaName)
        .AddScheme<ApiKeyAuthNOptions, ApiKeyAuthN>(ApiKeyAuthNDefaults.SchemaName, opt =>
        {
            opt.ApiKey = "Hello-World";
            opt.QueryStringKey = "key";
            opt.ClaimsIssuer = "API-Issuer";
        });

    services.AddAuthorization();
}
Enter fullscreen mode Exit fullscreen mode
Configure method - Startup.cs

In AddScheme we're configuring the service to use our Authentication handler. Next setup the Configure method to use Authentication and Authorization middlewares.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();

    app.UseRouting();

    app.UseAuthentication(); //adds authentication middleware
    app.UseAuthorization(); //adds authorization middleware

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync($"Hello World!{Environment.NewLine}");
            await WriteClaims(context);

        }).RequireAuthorization(); //forces user to be authenticated

        endpoints.MapGet("/anonymous", async context =>
        {
            await context.Response.WriteAsync($"Hello World!{Environment.NewLine}");
            await WriteClaims(context);
        }); //allow anonymous
    });
}

static async Task WriteClaims(HttpContext context)
{
    if (context.User.Identity.IsAuthenticated)
    {
        await context.Response.WriteAsync($"Hello {context.User.Identity.Name}!{Environment.NewLine}");

        foreach (var item in context.User.Identities.First().Claims)
        {
            await context.Response.WriteAsync($"Claim {item.Issuer} {item.Type} {item.Value}{Environment.NewLine}");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We also added WriteClaims method to see the user's Claims.

Let's run it.

Alt Text

Without Api Key

Alt Text

With Api Key

Making it easier to use

Let's create a extension method builder for our AddApiKey handler.
Create the file ApiKeyAuthNExtensions.cs with the following contents.

using APIAuthentication.Resource.Infrastructure;
using System;

namespace Microsoft.AspNetCore.Authentication
{
    public static class ApiKeyAuthNExtensions
    {
        public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, Action<ApiKeyAuthNOptions>? configureOptions)
            => AddApiKey(builder, ApiKeyAuthNDefaults.SchemaName, configureOptions);

        public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, string authenticationScheme, Action<ApiKeyAuthNOptions>? configureOptions)
            => builder.AddScheme<ApiKeyAuthNOptions, ApiKeyAuthN>(authenticationScheme, configureOptions);
    }
}
Enter fullscreen mode Exit fullscreen mode
ApiKeyAuthNExtensions.cs

This adds the extension method AddApiKey instead of calling AddScheme.

Change the Configure method in Startup class to use the new method.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(ApiKeyAuthNDefaults.SchemaName)
        .AddApiKey(opt =>
        {
            opt.ApiKey = "Hello-World";
            opt.QueryStringKey = "key";
        }); //new version

    //.AddScheme<ApiKeyAuthNOptions, ApiKeyAuthN>(ApiKeyAuthNDefaults.SchemaName, opt =>
    //{
    //    opt.ApiKey = "Hello-World";
    //    opt.QueryStringKey = "key";
    //}); //old version

    services.AddAuthorization();
}
Enter fullscreen mode Exit fullscreen mode
Method ConfigureServices - Startup.cs

This is it! Hope you like it. Leave a comment.

Source Code at https://github.com/ricardodemauro/article-APIAuthentication

Top comments (0)