I needed to add a very simple authorization mechanism to my API: use a query string parameter “api_key”, so that it’s compatible with Swagger (using Swashbuckle, there is a field “api_key” in the Swagger UI) and is easily callable through Ruby On Rails.
Following and adapting a nice tutorial, I have done the following.
Implement the authorization filter
Create an interface for your API key “getter”:
1 2 3 4 |
public interface IApiKeyProvider { string GetApiKey(); } |
Implement this interface; here it’s extremely simple:
1 2 3 4 5 6 7 |
public class WebConfigApiKeyProvider : IApiKeyProvider { public string GetApiKey() { return ConfigurationManager.AppSettings["api_key"]; } } |
Inject this interface through your dependency injector of choice. You don’t have to modify your controllers, which is great.
Then create a filter attribute to use this implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class ApiKeyAuthorizeAttribute : ActionFilterAttribute { private const string apiKeyParamName = "api_key"; public override void OnActionExecuting(HttpActionContext filterContext) { // get the IApiKeyProvider instance that has been injected (oh MVC4 that provides a dependency resolver, I love you) var provider = filterContext.ControllerContext.Configuration.DependencyResolver.GetService(typeof(IApiKeyProvider)) as IApiKeyProvider; // get the query string parameter ; // it must be parsed from the full query, since it's not actually part of the action, but an additional parameter var queryStringCollection = HttpUtility.ParseQueryString(filterContext.Request.RequestUri.Query); string apiKey = queryStringCollection[apiKeyParamName]; if (string.IsNullOrEmpty(apiKey) || apiKey != provider.GetApiKey()) { throw new HttpResponseException(HttpStatusCode.Unauthorized); } base.OnActionExecuting(filterContext); } } |
Now you just have to add the [ApiKeyAuthorize] attribute to your controller(s), and you now need to add the proper api_key query string parameter to all your requests.
Test the filter
A few things to test: that your class uses this attribute, and that the attribute does what it says it does.
Test the attribute presence
Here I’m using XUnit and FluentAssertions.
It’s just a matter of listing the attributes on the class, and checking that an attribute matching the one created exists.
1 2 3 4 5 6 7 8 |
[Fact] public void Archives_controller_has_attribute() { var attributes = typeof(ArchivesController).GetCustomAttributes(true); attributes.Count().Should().BeGreaterThan(0, "because there should be at least an attribute"); var attribute = attributes.OfType<ApiKeyAuthorizeAttribute>().Single(); attribute.Should().NotBeNull("because there should be an IApiKeyProvider on the archives controller"); } |
Test the attribute inner workings
What are we testing there? That the attribute throws a HttpResponseException when no parameter exists or when the value is wrong, and that it doesn’t throw an exception when it matches.
Setup the tests
Using XUnit and Moq, the test setup looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private ApiKeyAuthorizeAttribute attribute = new ApiKeyAuthorizeAttribute(); private HttpActionContext context = ContextUtil.CreateActionContext(); private Mock<IStorageModel> storageModelMock = new Mock<IStorageModel>(); private Mock<IApiKeyProvider> apiKeyProviderMock = new Mock<IApiKeyProvider>(); public ApiKeyAuthorizeAttributeTests() { apiKeyProviderMock.Setup(p => p.GetApiKey()).Returns("abc"); storageModelMock.Setup(s => s.CreationArchived(It.IsAny<string>())).Returns(true); storageModelMock.Setup(s => s.CreationArchives(It.IsAny<string>())).Returns(new List<Archive>()); storageModelMock.Setup(s => s.ListArchives(It.IsAny<int>(), It.IsAny<int>())).Returns(new List<Archive>()); InjectionSetup.Register(context.ControllerContext.Configuration, storageModelMock.Object, apiKeyProviderMock.Object); } |
The ContextUtil.CreateActionContext method can be picked from the ASP.Net source. The corresponding tests can be found here.
My InjectionSetup.Register is a unit-test-specific injection that allows to use a specific instance instead of creating one, here using LightInject:
1 2 3 4 5 6 7 8 |
public static void Register(HttpConfiguration config, IStorageModel storageModel, IApiKeyProvider apiKeyProvider) { var container = new ServiceContainer(); container.Register<IStorageModel>((factory) => storageModel); container.Register<IApiKeyProvider>((factory) => apiKeyProvider); container.RegisterApiControllers(); container.EnableWebApi(config); } |
Test the attribute
Still using XUnit and FluentAssertions, three simple tests allow to check that the responses are what is expected:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[Fact] public void Missing_key_throws_error() { context.Request.RequestUri = new Uri("http://localhost/api/archives"); Action act = () => attribute.OnActionExecuting(context); act.ShouldThrow<HttpResponseException>("because a non-authorized action should throw an exception").And.Response.StatusCode.Should().Be(HttpStatusCode.Unauthorized, "because the request should not be authorized"); } [Fact] public void Wrong_key_throws_error() { context.Request.RequestUri = new Uri("http://localhost/api/archives?api_key=wrong"); Action act = () => attribute.OnActionExecuting(context); act.ShouldThrow<HttpResponseException>("because a wrong API key should throw an exception").And.Response.StatusCode.Should().Be(HttpStatusCode.Unauthorized, "because the request should not be authorized"); } [Fact] public void Right_key_passes() { context.Request.RequestUri = new Uri("http://localhost/api/archives?api_key=abc"); Action act = () => attribute.OnActionExecuting(context); act.ShouldNotThrow<HttpResponseException>("because an authorized action should not throw an exception"); } |