Friday, March 20, 2015

CSRF in AngularJS/Web API on Web Forms (ng-resource)

I came across an interesting site configuration where there was need of anti forgery tokens (due to Cross-Site Request Forgery exploits), however it was a web forms implementation and AngularJS was making Web API calls via $resource (both GET and POST).
app.factory('LoginProvider', function ($resource) {
 return $resource('/api/login/:id', { id: '@id' },
  { 'save': { method: 'POST', headers: { 'X-XSRF-Token': angular.element('input[name="__RequestVerificationToken"]').attr('value') } } });
});

app.factory('OtherProvider', function ($resource) {
 return $resource('/api/OtherProvider/:id', { id: '@id' },
  {'get': { method: 'GET', headers: { 'X-XSRF-Token': angular.element('input[name="__RequestVerificationToken"]').attr('value') } }});
});
using System;
using System.Linq;
using System.Net.Http;
using System.Web.Helpers;
using System.Web.Http.Filters;

namespace Web.Common.AntiForgery
{
    public sealed class AntiForgeryTokenAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
        {
            if (actionContext == null)
            {
                throw new ArgumentNullException("actionContext");
            }
            var headers = actionContext.Request.Headers;
            var cookie = headers
                .GetCookies()
                .Select(c => c[AntiForgeryConfig.CookieName])
                .FirstOrDefault();
            var tokenFromHeader = headers.GetValues("X-XSRF-Token").FirstOrDefault();
            System.Web.Helpers.AntiForgery.Validate(cookie != null ? cookie.Value : null, tokenFromHeader);

            base.OnActionExecuting(actionContext);
        }
    }
}
<%# AntiForgery.GetHtml() %>
namespace Web.Services
{
    [RoutePrefix("api/login")]
    [Route("{action=Post}")]
    public class LoginController : ApiController
    {
        [AntiForgeryToken]
        [ResponseType(typeof(LoginResponseModel))]
        [Route("")]
        public IHttpActionResult Post([FromBody]LoginRequestModel request)
        {
            var response = new LoginResponseModel();
            
            // Business Logic
            
            return Ok(response);
        }        
    }
}
The code on the master page (or can be placed on the form) is what puts the hidden field containing the token on the page. The ActionFilterAttribute is defined as "AntiForgeryTokenAttribute", but on the Web API side the Attribute is dropped and it is simply "[AntiForgeryToken]".

On the AngularJS side, the $resource POST request adds in the header "X-XSRF-Token", which connects with the hidden field on the master page (or layout). I place it here instead of in the form in case there are multiple forms on the same page that require anti forgery tokens.

1 comment:

  1. AntiForgery.GetHtml() will require <%@ Import Namespace="System.Web.Helpers" %>

    ReplyDelete