When building APIs for public web apps, we cannot add the authorization filters to the APIs because the end users do not have to authenticate. We can however add some security measures to the API which would protect it by allowing only requests from the same origin with an anti-forgery token thus allowing us to handle CSRF (Cross Site Request Forgery) threats. Let's take a look at how we can do this.
Common Scenario
We have a public web app that allows users to send a contact request from the browser. We do not want to leave our API open to the world. We just want same origin requests to have access to our API. As an example, our web app is hosting a contact form as shown below. The contact request is sent by calling an API with anonymous access.
Contact Page |
Anti-Forgery Token
An anti-forgery or request verification token is used to protect a resource from a cross-site request forgery (CSRF). The ASP.NET MVC Framework provides an HTML helper that creates the token for us. This is what we can use to insert a token in our contact form.
HTML Markup Design Mode
We use the Html helper with Razor syntax in our HTML markup as shown below:
<form method="post" id="contact" novalidate name="contactForm"> @Html.AntiForgeryToken() |
HTML Markup Output
After the page is rendered on the browser, the result of the AntiForgeryToken call produces the following HTML mark-up:
<input name="__RequestVerificationToken" type="hidden" value="AMJ-A-9FvyoZMVpsapx2Dxr4drOUX_616Evo-Xxaq2ooscMXKOZG-qdQ3gzI0y4cekVotAaTql69J5yD1DCGqK4zzYM6c36Gm8DEtuxQkbw1" /> |
The helper call creates a cookie (_RequestVerificationToken) and a hidden input field with the same name and value. The idea is that this token becomes a signature of the forms that are served by our servers. When the form is post back, both the cookie and input field are sent to the server for validation. This is when the anti-forgery validation takes place on a typical MVC controller action. In our case, we are using an API Controller, so let's take a look at this approach.
AngularJS Module:
To test our API, we create an AngularJS client. Our AngularJS module has a controller and service. The controller handles the interaction with the contact view. The service manages the request to our contact API.
//angular module var app = angular.module('appContact', ['ngRoute', 'ui.bootstrap']); //angular service app.factory('app.svc.contact', ['$http', svcContact]); //contact controller app.controller('app.ctrl.contact', ['$scope', '$uibModal', 'app.svc.contact', 'app.const.contact','$log', ctrlContact]); |
Since we are using an AngularJS service to send our requests instead of submitting a form, our approach is a bit different. We still need to send the token. but when we send the request via our service, we need to add the token in the request header. This is what an action filter will be parsing and validating on the server.
(function (angular) { 'use strict'; var app = angular.module('appContact'); app.factory('app.svc.contact', ['$http', svcContact]); function svcContact($http) { var baseRoute = '/api/contact/'; return { add: add } function add(contact) { var token = contact.token; if (token && contact.sendToken) { $http.defaults.headers.post['X-XSRF-Token'] = contact.token; } else { delete $http.defaults.headers.post['X-XSRF-Token']; } return $http.post(baseRoute+"send", contact); } } })(angular); |
In our contact form, there is a checkbox that is bound to the model property sendToken. If the option to send the token on the form is not checked (defaults to true to send the token), we remove the token from the header. This allows us to test the case with no token.
Validate Token Action Filter
The action filter can be used in our API Controller to block access to the APIs when the token is not valid. Let's take a look at what the code for our action filter looks like:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public sealed class ValidateHttpAntiForgeryTokenAttribute : ActionFilterAttribute { private const string HeaderTokenName = "X-XSRF-Token"; public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext) { if (actionContext == null) { throw new ArgumentNullException("actionContext"); } var headers = actionContext.Request.Headers; IEnumerable<string> tokens; try { if (headers.TryGetValues(HeaderTokenName, out tokens)) { var headerToken = tokens.FirstOrDefault(); var cookie = headers.GetCookies().Select(c => c[AntiForgeryConfig.CookieName]).FirstOrDefault(); var cookieToken = cookie != null ? cookie.Value : null; AntiForgery.Validate(cookieToken, headerToken); } else { string msg = HttpStatusCode.ExpectationFailed.ToString()+ " - " + HeaderTokenName; throw new InvalidOperationException(msg); } } catch{ actionContext.Response = new HttpResponseMessage { RequestMessage = actionContext.ControllerContext.Request, StatusCode = HttpStatusCode.Forbidden, ReasonPhrase = "Invalid Token" }; } base.OnActionExecuting(actionContext); } } |
The code above checks the header for the X-XSRF-Token value and the anti-forgery cookie. It then calls the AntiForgery.Validate helper method to validate that both values are matching. If this is not the case, or there is no token found on the header and exception is thrown and a forbidden (403) error is returned essentially blocking the access to the API.
API Controller with Action Filter
Since our action filter has been defined with the attribute usage of class or method, we can add the attribute at the method or class level in our API controller. In our case, we are using it at the class level. By decorating our class with this attribute (ValidateHttpAntiForgeryToken), we are adding a declarative security validation to all the methods in the class.
[RoutePrefix("api/contact")] [helper.ValidateHttpAntiForgeryToken] public class ContactApiController : ApiController { // POST: api/Contact [HttpPost] [ResponseType(typeof(Contact))] [Route("send")] public async Task<IHttpActionResult> send(Contact contact) { IHttpActionResult result = null; if (!ModelState.IsValid) { result = BadRequest(ModelState); } else { //simulate delay for proceesing information await Task.Delay(1500); contact.Created = DateTime.Now; //TODO process contact information result = Ok(contact); } return result; } } |
If the token is not valid, the action filter does not allow access to the api/contact/send route. In the code above, the send method just simulates some asynchronous action (delay) and adds the created date value to the contact request. It then just returns the contact information back to the client.
Demo the Contact Module
When we run the demo application, we can test with or without the token. If we look at the browser dev tools, we can examine the response for each of the requests. This can show us how a request that uses the token returns a HTTP status of 200 (success). Compare it to the request without the token which returns a status of 403 (forbidden – or no token).
When we look at the successful request header, we can see that both the cookie and custom header entries are sent. (highlighted below)
Not a Jason Web Token
We should note that the anti-forgery token is not a Jason Web Token (JWT) which is used for OAuth authentication when there is a login requirement. This is just a validation token to ensure that a request comes from the same origin (domain) and from an authorized client app.
I hope this is able to show you a simple way to protect your public APIs.
I hope this is able to show you a simple way to protect your public APIs.