Use API key authentication in ASP.NET Core
Developers often need to authenticate Web API calls at two levels. You might want to authenticate individual users while making an API call or you might want to authenticate the client that is trying to call an API. The former is can be implemented via a token based (JWT) scheme and latter can be implemented using what is known an API key authentication.
At times you want to expose your Web APIs only to certain clients. In such cases you want to ensure that only a valid client application is making calls to an API. A simple approach to such a validation is to use a secret key that is known only to the Web API and the client application. This secret key is passed from the client to the API with each and every call. The API validates the key send by the client and executes the processing only when the key is valid.
There could be various approaches to pass key from the client to the server such as query string, request body and HTTP header. There could be different approaches for validating the key passed by the client such as API code, authorization filter, and middleware. In the example discussed in the remainder of this article we use HTTP header to pass a key from the client to the API and then validate that key using API code and a custom attribute.
Begin by creating a new ASP.NET Core application using Empty project template.
Once the project gets created, open the appsettings.json file and add a custom Authentication section as shown below:
"Authentication": {
"Key": "This is an API auth key",
"Header": "X-API-AUTH-KEY"
}
The Authentication section stores two keys : Key and Header. The Key is used to store a secret key that is known only to the API and the client app. For the sake of simplicity I have put a plain string here. You should use some encrypted value for more security. The Header is used to store a name of a custom HTTP header that will carry the Key from client to the API. The client will set this HTTP header and the API will read it during the validation process.
Now add a class called ApiKeyValidator to the project. Write the following code in the ApiKeyValidator class.
public class ApiKeyValidator
{
private readonly IConfiguration config;
public ApiKeyValidator(IConfiguration config)
{
this.config = config;
}
public bool IsValid(string clientKey)
{
var expectedKey = config["Authentication:Key"];
return clientKey == expectedKey;
}
}
We inject IConfiguration in the ApiKeyValidator so that we can retrieve the API key stored in the configuration file. The IsValid() method accepts a key as sent by the client. Inside, it checks this key against the key stored in the configuration file. If they match IsValid() returns true; otherwise it returns false.
You need to register ApiKeyValidator class with the DI container. So, open Program.cs and add the following code in it.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddControllersWithViews();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ApiKeyValidator>();
var app = builder.Build();
app.MapControllers();
app.MapDefaultControllerRoute();
app.Run();
Now that our ApiKeyValidator is ready, we can use it in an API. Add a new API Controller named ValuesController to the project.
Inject IConfiguration and ApiKeyValidator in the Web API controller using the following code:
private readonly IConfiguration config;
private readonly ApiKeyValidator validator;
public ValuesController(IConfiguration config,
ApiKeyValidator validator)
{
this.config = config;
this.validator = validator;
}
Then add the following Get() action to the ValuesController.
[HttpGet]
public IActionResult Get()
{
var key = Request.Headers
[config["Authentication:Header"]];
if (!validator.IsValid(key))
{
return Unauthorized();
}
var data = new List<string> {
"Hello World",
"Hello Galaxy",
"Hello Universe"
};
return Ok(data);
}
Notice the code shown in bold letters. We retrieve the HTTP header name from the configuration file (X-API-AUTH-KEY in this example). We then retrieve the header value by reading it from the Request.Headers collection. Finally, the key is validated using the IsValid() method of the ApiKeyValidator.
If the key validation fails, we sent HTTP status code 401 - Unauthorized using the Unauthorized() method. If the key validation succeeds we return HTTP status code 200 - OK along with a List containing a few string values -- Hello World, Hello Galaxy, and Hello Universe.
If you want to validate a key inside a minimal API, the equivalent code you need to add the Program.cs would be :
app.MapGet("/minimalapi/values",
(HttpContext context, ApiKeyValidator validator) =>
{
string key = context.Request.Headers
[builder.Configuration
["Authentication:Header"]];
if (!validator.IsValid(key))
{
return Results.Unauthorized();
}
var data = new List<string> {
"Hello World",
"Hello Galaxy",
"Hello Universe" };
return Results.Ok(data);
});
The above code is quite similar to the Get() action but there are a few differences. Firstly, we inject HttpContext and ApiKeyValidator in the MapGet() handler function. Secondly, we access Request.Headers collection using the HttpContext. Finally, we return HTTP status codes using Results class and its static Unauthorized() and Ok() methods.
To test our controller based API and minimal API, we will create MVC controller that acts as the client for our APIs. So, add a new MVC controller to the project named HomeController.
Now add Index() action in the HomeController as shown below:
public async Task<IActionResult> Index()
{
var client=new HttpClient();
client.DefaultRequestHeaders.Add
("X-API-AUTH-KEY",
"This is an API auth key");
var data = await client.GetFromJsonAsync
<List<string>>
("https://localhost:7177/api/values");
return View(data);
}
We create a new HttpClient object and set its X-API-AUTH-KEY header to "This is an API auth key". We call the Get() action using GetFromJsonAsync() method. Make sure to replace your port number in place of 7177 shown above.
Add the Index.cshtml view file to the Views -- Home folder.
Write the following code in the Index view.
@model List
<h1>API returned...</h1>
@foreach(var item in Model)
{
<h2>@item</h2>
}
Here, we have supplied a valid key from the client. So, a sample run will look like this:
Try supplying an invalid key :
client.DefaultRequestHeaders.Add
("X-API-AUTH-KEY",
"This is an invalid API auth key");
And this time you should get this error message :
Also confirm the success and erroneous behavior with the minimal API by changing the HttpClient URL to /minimalapi/values.
So far so good. We have successfully authenticated the API client using the approach discussed above. However, you might have guessed that the validation logic used by the Get() action and the MapGet() handler needs to be repeated for all the other API actions / handlers. You can avoid this code repetition by wrapping the validation logic in a custom attribute. Let's see how.
We will first need to create an authorization filter and then expose that filter through an attribute.
Add a new class in the project called ApiKeyAuthorizationFilter and write the following code in it.
public class ApiKeyAuthorizationFilter :
IAuthorizationFilter
{
private readonly ApiKeyValidator validator;
private readonly IConfiguration config;
public ApiKeyAuthorizationFilter
(ApiKeyValidator validator,
IConfiguration config)
{
this.validator = validator;
this.config = config;
}
public void OnAuthorization
(AuthorizationFilterContext context)
{
string clientKey = context.HttpContext.
Request.Headers[config
["Authentication:Header"]];
if(!validator.IsValid(clientKey))
{
context.Result = new UnauthorizedResult();
}
}
}
Notice the code shown in bold letters that implements the OnAuthorization() method. Inside, we retrieve the API key from the Headers collection and perform validation as before. If the key is invalid we return UnauthorizedResult.
Next, add a class called RequiresApiKeyAttribute to the project and add t he following code in it.
public class RequiresApiKeyAttribute :
ServiceFilterAttribute
{
public RequiresApiKeyAttribute()
: base(typeof(ApiKeyAuthorizationFilter))
{
}
}
We simply create a custom attribute that inherits from ServiceFilterAttribute base class. If you are not familiar with ASP.NET Core filters consider reading this from the official documentation.
Once the RequiresApiKeyAttribute class is created we can apply it on the Get() action or the MapGet() handler as shoen below:
[HttpGet]
[RequiresApiKey]
public IActionResult Get()
{
var data = new List<string> {
"Hello World",
"Hello Galaxy",
"Hello Universe" };
return Ok(data);
}
app.MapGet("/minimalapi/values",
[RequiresApiKey]
(HttpContext context,
ApiKeyValidator validator) =>
{
var data = new List<string> {
"Hello World",
"Hello Galaxy",
"Hello Universe" };
return Results.Ok(data);
});
As you can see, the Get() action and the MapGet() handler no longer have any API key validation logic. Instead, they are decorated with [RequiresApiKey] attribute. The [RequiresApiKey] attribute now takes care of performing the key validation and return an error in case the key is invalid.
Make sure that you add the newly created filter to DI like this:
builder.Services.
AddScoped<ApiKeyAuthorizationFilter>();
Run the application as before and check whether you get outcome identical to the previous runs.
That's it for now! Keep coding!!