We will learn create code (and understand how it works) to handle API Key authentication with just three lines of code extending native Authentication mechanism.
In this article, we're going to create the code (and understand how it works) to handle API Key authentication with just three lines of code extending the native Authentication mechanism. We want a simple and stupid solution and not some crazy implementation using MVC [Attributes]
or any customized middleware to handle the Authentication.
services.AddAuthentication(ApiKeyAuthNDefaults.SchemaName)
.AddApiKey(opt =>
{
opt.ApiKey = "Hello-World";
opt.QueryStringKey = "key";
});
Solution - Adding API Key Authentication Service
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 for us needing API Key Authentication on daily basis. But 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 allows us to extend it and create our validation logic.
With the AddScheme
builder, we're going to implement the APIKey Authentication.
Everything begins with the services.AddAuthentication
code. This builder provides 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 configurations 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; }
}
}
ApiKeyAuthNOptions.cs
The 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();
}
}
}
Initial implementation of ApiKeyAuthN.cs
The class AuthenticationHandler
is responsible for 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();
if (string.IsNullOrEmpty(apiKey))
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)));
}
return Task.FromResult(AuthenticateResult.Fail
($"Invalid API Key provided."));
}
HandleAuthentication - ApiKeyAuthN.cs
protected string ParseApiKey()
{
if (Request.Query.TryGetValue(Options.QueryStringKey, out var value))
return value.FirstOrDefault();
return string.Empty;
}
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;
}
BuildPrincipal method - ApiKeyAuthN.cs
The implementation 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.
Wrapping 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();
}
Configure method - Startup.cs
In AddScheme
, we're configuring the service to use our Authentication handler. Next, set up 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();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync($"Hello World!{Environment.NewLine}");
await WriteClaims(context);
}).RequireAuthorization();
endpoints.MapGet("/anonymous", async context =>
{
await context.Response.WriteAsync($"Hello World!{Environment.NewLine}");
await WriteClaims(context);
});
});
}
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}");
}
}
}
Startup.cs - Configure
We also added WriteClaims
method to see the user's Claims.
Let's Run It
Without API Key
With API Key
Making It Easier to Use
Let's create an 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);
}
}
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";
});
services.AddAuthorization();
}
Method ConfigureServices - Startup.cs
This is it! Hope you like it. Leave a comment.
Source Code
Disclaimer: There is a good implementation in the format of nuget package here: https://github.com/mihirdilip/aspnetcore-authentication-apikey.