The ability to create your own middleware comes in pretty handy when creating MVC 6 APIs as it allows the inspection and processing of the requests/responses flowing into and out of the application.

A few of the most useful ones I’ve used are below, a good guide to using middleware in ASP.NET 5 can be found here.

Authorization

When building an API it’s generally a good idea to secure it in some way, there’s many different ways of doing this but one of the most common methods clients ask for is to have a key of some type in the header. Inspection of the headers can be done with something like the following AuthorizationMiddleware.cs class.

public class AuthorizationMiddleware
{
	private readonly RequestDelegate _next;
	private readonly IConfiguration _config;
	
	public AuthorizationMiddleware(RequestDelegate next, IConfigurationRoot config)
	{
		_next = next;
		_config = config;
	}
	
	public async Task Invoke(HttpContext context)
	{
		// Only check POSTs, other requests ignored by controller
		if (context.Request.Method == "POST")
		{
			// Need to convert to uppercase as Advanced Rest Client seems to lowercase all custom keys
			// Only really a problem for testing rather than live
			if (!context.Request.Headers.Keys.Contains("user-key"))
			{
				context.Response.StatusCode = 400; //Bad Request
				await context.Response.WriteAsync(JsonConvert.SerializeObject(new Error()
				{
					ErrorMessage = "Request is missing API user key header"
				}));
				
				return;
			}
			else
			{
				int? userId = Common.CheckAuthorized(context.Request.Headers["user-key"], _config.GetConnectionString("AzureConnection"));
				context.Items["UserId"] = userId;
				
				if (userId == null)
				{
					context.Response.StatusCode = 401; //Unauthorized
					await context.Response.WriteAsync(JsonConvert.SerializeObject(new Error()
					{
						ErrorMessage = "Invalid API user key"
					}));
					
					return;
				}
			}
		}
		await _next.Invoke(context);
	}
}

IP Address Whitelist

Having only an approved whitelist of IP addresses that can access the API is also a good idea, in this case they’re stored as a comma seperated list in appsettings.json but they could also be stored in a SQL database or somewhere similar.

public class IpRestrictionMiddleware
{
	private readonly RequestDelegate _next;
	private readonly IConfiguration _config;
	
	public IpRestrictionMiddleware(RequestDelegate next, IConfigurationRoot config)
	{
		_next = next;
		_config = config;
	}
	
	public async Task Invoke(HttpContext context)
	{
		var ipAddress = (string)context.Connection.RemoteIpAddress?.ToString();
		
		if (!SplitIpAddresses(_config["IpSecuritySettings:AllowedIPs"]).Contains(ipAddress))
		{
			context.Response.StatusCode = 403; return;
		}
		
		await _next.Invoke(context);
	}
	
	public static List<string> SplitIpAddresses(string IpAddresses)
	{
		return !string.IsNullOrEmpty(IpAddresses) ? IpAddresses.Split(',').ToList() : new List<string>();
	}
}

Failed Request Logging

Capturing every request to an API can quickly produce a giant database and won’t really provide any useful information but capturing only failed requests can certainly be handy for troubleshooting.

public class CaptureRequestMiddleware
{
	private readonly RequestDelegate _next;
	
	public CaptureRequestMiddleware(RequestDelegate next)
	{
		_next = next;
	}
	
	public async Task Invoke(HttpContext context)
	{
		// Only log if is a POST request
		if (context.Request.Method == "POST")
		{
			MemoryStream requestBodyStream = new MemoryStream();
			Stream originalRequestBody = context.Request.Body;
			
			await context.Request.Body.CopyToAsync(requestBodyStream);
			requestBodyStream.Seek(0, SeekOrigin.Begin);
			
			string requestBodyText = new StreamReader(requestBodyStream).ReadToEnd();
			context.Items["RequestBodyText"] = requestBodyText;
			
			requestBodyStream.Seek(0, SeekOrigin.Begin);
			context.Request.Body = requestBodyStream;
			
			await _next.Invoke(context);
			context.Request.Body = originalRequestBody;
		}
	}
}

Implementation

public static class MiddlewareExtensions
{
	public static IApplicationBuilder UseIpRestrictionMiddleware(this IApplicationBuilder builder)
	{
		return builder.UseMiddleware<IpRestrictionMiddleware>();
	}
	
	public static IApplicationBuilder UseAuthorizationMiddleware(this IApplicationBuilder builder)
	{
		return builder.UseMiddleware<AuthorizationMiddleware>();
	}
	
	public static IApplicationBuilder UseCaptureRequestMiddleware(this IApplicationBuilder builder)
	{
		return builder.UseMiddleware<CaptureRequestMiddleware>();
	}
}

These extensions should then be added in to the Configure method in startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	loggerFactory.AddConsole(Configuration.GetSection("Logging"));
	loggerFactory.AddDebug();
	
	// Add middleware app.UseIpRestrictionMiddleware();
	app.UseAuthorizationMiddleware();
	app.UseCaptureRequestMiddleware();
	app.UseMvc();
}

Values from the middleware can be passed back to methods in the controller by using ControllerContext.HttpContext, this way, the request body which isn’t available to the controller can logged if it doesn’t match to the expected model. Errors relating to this are accessable from the ModelState.

public IActionResult Post([FromBody]EmailAddress emailAddress)
{
	if (ModelState.IsValid)
	{
		int userId = (int)ControllerContext.HttpContext.Items["UserId"];
		Common.InsertEmail(userId, emailAddress.Email, _config.GetConnectionString("AzureConnection"));
		
		return StatusCode(200);
	}
	else
	{
		string requestBodyText = ControllerContext.HttpContext.Items["RequestBodyText"].ToString();
		int requestId = Common.LogErrorRequest(requestBodyText, _config.GetConnectionString("AzureConnection"));
		
		IEnumerable errors = ModelState.SelectMany(x => x.Value.Errors, (y, z) => z.Exception.Message);
		
		foreach (string error in errors)
		{
			Common.LogErrorMessage(requestId, error, _config.GetConnectionString("AzureConnection"));
		} return BadRequest(errors);
	}
}

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *