SPA Authentication and CSRF MVC4 AntiForgery Implementation

Introduction

This post can be rather long and actually contains 2 concepts. I will share my experiments in porting ASP.Net MVC4’s authentication to work with the HotTowel SPA template. As well, in implementing the AntiForgery token approach of preventing cross-site forgery attacks to a Single Page Application, assuming you have a use-case for using these 2 technologies. Let’s get started…

SPA Authentication and the AntiForgeryToken tokens

I started creating the back-end Web API that compliments/subtitutes the default “Account” controller generated by Visual Studio 2012 ” ASP.NET MVC 4 project”.  ASP .Net MVC4 nowadays comes baked in with the SimpleMembership provider when you create a web project. It is much simpler and easily expandable to suit your needs, compared to the Universal Membership Providers that shipped with older Microsoft web project technologies, and I won’t discuss it here.

I decorated the entire class with the [Authorize] attribute so that only authenticated sessions can make calls to the web methods exposed by this web api controller by default. Of course, the Login method should allow anonymous access, so that’s why I have this method explicitly decorated with [AllowAnonymous] filter. You will also notice that the Login method is decorated with the [SpaAntiForgeryTokenAttribute] filter. This is essentially the same with the official ASP .Net MVC’s [ValidateAntiForgeryToken] filter, but is configured to process Web API methods instead. More on this filter later.

Right off the bat, the Login web api method tries to mitigate CSRF attack by requiring the correct AntiForgery tokens being passed in. The client code later will show you how this token(s) are passed.

Upon authentication, extra steps are taken to generate the new tokens, since a user has been authenticated already. These tokens are then “passed back” to the authenticated client in which the client will have to use everytime it makes web api calls that you choose to protect. This is the trick – otherwise, if you keep using the “unauthenticated tokens” after you had logged in,  nothing will work…..

    [Authorize]
    public class AuthController : ApiController
    {
        [AllowAnonymous]
        [HttpPost]
        [SpaAntiForgeryTokenAttribute]
        public AuthResult Login(LoginModel model)
        {
            string cookieToken = "", formToken = "";
            if (WebSecurity.Login(model.UserName, model.Password, persistCookie: model.RememberMe))
            {
                HttpContext.Current.User = new GenericPrincipal(new GenericIdentity(model.UserName), null);
                AntiForgery.GetTokens(null, out cookieToken, out formToken);
                // obtain the new "logged in" anti-forgery cookie from here
                HttpContext.Current.Response.Cookies[AntiForgeryConfig.CookieName].Value = cookieToken;
                return new AuthResult() { Result = true, Form = formToken };
            }
            else
            {
                return new AuthResult { Result = false, Form = formToken };
            }

        }
    }

Of course when you create your LogOff method, you will have to create the new tokens as well

  //Log Off
  HttpContext.Current.User = new GenericPrincipal(new GenericIdentity(""), null);
  //generate, then return the new UnAuthenticated token

and pass it back to the client if you intend to still have some unauthenticated but CSRF protected sessions going forward.

Initial tokens

To make it simpler, let’s assume that upon hitting the website, the client is not authenticated yet. So what I did, is to spit out the initial token as part of the “single page”.

Home/Index controller:

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            string cookieToken, formToken;
            AntiForgery.GetTokens(null, out cookieToken, out formToken);
            HttpCookie cookie = new HttpCookie(AntiForgeryConfig.CookieName, cookieToken);
            HttpContext.Response.Cookies.Add(cookie);
            return View(new AuthResult() { Result = true, Form = formToken });
        }

    }

Somewhere in Index.cshtml (inside a script tag):

var my = my || {};
        my.root = '@Url.Content("~/")';
        my.initLoggedIn = ('@User.Identity.IsAuthenticated.ToString().ToLower()' === 'true');
        my.initUser = '@User.Identity.Name';
        my.FormToken = '@Model.Form';
	// .....

my.FormToken holds the form token throughout the entire application life cycle.

So, everytime I make an AJAX call, I send this form token – for the [SpaAntiForgeryTokenAttribute] to validate…

I modified durandalJS’s http module, so that it always carries the form token for every ajax call to my web api methods… For example:

        post: function (url, data) {
            var csrfToken = my.FormToken;
            return $.ajax({
                url: url,
                data: ko.toJSON(data),
                type: 'POST',
                headers: { __RequestVerificationToken: csrfToken },
                contentType: 'application/json',
                dataType: 'json'
            });
        }

The SpaAntiForgeryTokenAttribute filter

The code is pretty much self-explanatory (I think). I derived this code from Stephen Walther’s excellent post about CSRF as well. It basically checks that the incoming request has the correct form (via header) and cookie tokens.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class,
 AllowMultiple = false, Inherited = true)]
 public class SpaAntiForgeryTokenAttribute : System.Web.Http.Filters.ActionFilterAttribute
 {

public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
 {
 // IE 8 shit, names are lower-cased
 if (!actionContext.Request.Headers.Any(z => z.Key.Equals(AntiForgeryConfig.CookieName, StringComparison.OrdinalIgnoreCase)))
 {
 actionContext.Response =
 actionContext.Request.CreateResponse(HttpStatusCode.ExpectationFailed);
 actionContext.Response.ReasonPhrase = "Missing token";
 return;
 }

var headerToken = actionContext
 .Request
 .Headers
 .Where(z => z.Key.Equals(AntiForgeryConfig.CookieName, StringComparison.OrdinalIgnoreCase))
 .Select(z => z.Value)
 .SelectMany(z => z)
 .FirstOrDefault();

var cookieToken = actionContext
 .Request
 .Headers
 .GetCookies()
 .Select(c => c[AntiForgeryConfig.CookieName])
 .FirstOrDefault();

// check for missing cookie or header
 if (cookieToken == null || headerToken == null)
 {
 actionContext.Response =
 actionContext.Request.CreateResponse(HttpStatusCode.ExpectationFailed);
 actionContext.Response.ReasonPhrase = "Missing token null";
 return;
 }

// ensure that the cookie matches the header
 try
 {
 AntiForgery.Validate(cookieToken.Value, headerToken);
 }
 catch
 {
 actionContext.Response =
 actionContext.Request.CreateResponse(HttpStatusCode.ExpectationFailed);
 actionContext.Response.ReasonPhrase = "Invalid token";
 return;
 }

base.OnActionExecuting(actionContext);
 }
 }

Conclusion

I checked the ASP. Net MVC roadmap, and it looks like they are going to put more security features on Web API, so I guess stay tuned! as more goodies are bound to come, and new things to learn.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.
Tagged with: , , , ,
Posted in SPA