Custom Authentication

Disclaimer, this example uses a static API Key. Rather look at OAuth 2.0 Client Credentials Flow to request access tokens that would expire.

Background

We needed a simple way to secure an internal Notifications API. It needed to act as a message broker & queue. The flow would be to have actions like

  1. Queue email to be sent, this would just database it in a queue.
  2. Process email queue, this would read the queue table and send the emails / updating a done flag.

The simplest way to secure this is to add an Api-Key to the request header and check its existance and equality in a filter.

Setup: NotificationApi

Filters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace NotificationApi.Filters
{
public class ApiKeyValidationAttribute : FilterAttribute, IAuthorizationFilter
{
public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
try
{
var expected = ConfigurationManager.AppSettings.Get("apikey");
var actual = actionContext.Request.Headers.GetValues("Api-Key").FirstOrDefault();
if (String.IsNullOrWhiteSpace(actual) || actual != expected)
{
throw new Exception();
}
}
catch
{
// some logging would be sweet

actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Missing or invalid Api-Key");
var source = new TaskCompletionSource<HttpResponseMessage>();
source.SetResult(actionContext.Response);
return source.Task;
}
return continuation();
}
}
}

App start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using NotificationApi.Filters;
using System.Web.Http;
using System.Net.Http.Headers;

namespace NotificationApi
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);

// JSON responses please
config.Formatters.JsonFormatter.SupportedMediaTypes
.Add(new MediaTypeHeaderValue("text/html"));

// Add the filter to check the APIKEY
config.Filters.Add(new ApiKeyValidationAttribute());
}
}
}

Controllers

No Authorize attribute/annotation is required on the controllers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/// <summary>
/// SelectList - view whats in the Queue table. (email_queue.processed = false)
/// http://localhost:50829/api/email/selectlist
/// </summary>
/// <returns></returns>
[HttpGet]
public HttpResponseMessage SelectList()
{ ... }

/// <summary>
/// Insert a new Email into the Queue (dbo.email_queue)
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
[HttpPost]
public HttpResponseMessage Insert([FromBody] EmailModel obj)
{ ... }

/// <summary>
/// Process whats in the Queue table
/// http://localhost:50829/api/email/process
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
// [HttpPut] - not really a PUT as nothing is sent 'TO' 'Process()' to be persisted
[HttpGet]
public HttpResponseMessage Process()
{ ... }

Setup: Consumer

1
2
3
4
5
6
7
var url = "http://localhost:50829/api/email/insert";

var http = (HttpWebRequest)WebRequest.Create(new Uri(url));
http.Accept = "application/json";
http.ContentType = "application/json";
http.Method = "POST";
http.Headers.Add("Api-Key", ConfigurationManager.AppSettings["apiKey"]);

References