API Key Authentication - Extending the native implementation
Tags: csharp, authentication, beginners
Published: 2021-03-28 22:22:46

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.

```csharp
services.AddAuthentication(ApiKeyAuthNDefaults.SchemaName)
    .AddApiKey(opt => //here is our handler
    {
        opt.ApiKey = "Hello-World";
        opt.QueryStringKey = "key";
    });
```
###### 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`.

```csharp
using Microsoft.AspNetCore.Authentication;

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

        public string QueryStringKey { get; set; }
    }
}
```
###### ApiKeyAuthNOptions.cs

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

```csharp
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();
        }
    }
}
```
###### 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.

```csharp
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
}
```

###### HandleAuthentication - ApiKeyAuthN.cs

```csharp
protected string ParseApiKey()
{	
    if (Request.Query.TryGetValue(Options.QueryStringKey, out var value))
        return value.FirstOrDefault();

    return string.Empty;
}
```
###### ParseApiKey method - ApiKeyAuthN.cs

```csharp
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;
}
```
###### 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.

```csharp
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();
}
```
###### 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*.

```csharp
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}");
        }
    }
}
```
We also added WriteClaims method to see the user's Claims.

## Let's run it.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jiulgrx4ztsg4pnbkh03.png)

###### Without Api Key

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j4awvoejr083ohd4jaqf.png)

###### 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.

```csharp
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);
    }
}
```
###### ApiKeyAuthNExtensions.cs

This adds the extension method `AddApiKey` instead of calling `AddScheme`.

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

```csharp
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();
}
```
###### Method ConfigureServices - Startup.cs

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

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