Introduction
In this article, I'm going to show how you can use the well known antiforgery infrastructure from ASP.NET MVC with ASP.NET Web API and Angular.
Background
First of all, let's talk about how the anti XSRF infrastructure works in MVC. When the Html.AntiForgeryToken
line appears in the Razor markup, then two things happen: in the HTML, there will be a hidden input element which stores one half of the token and also a cookie will be attached to the response with the other token. After that, when the user submits the form, then the token from the hidden field will be on the request body and the cookie will naturally go as a cookie. :) On the server side, the AntiForgeryToken
class will take care of validating if the tokens are correct or not.
Okay, now let's switch to SPA. In this case, we don't have a server side token as the markup will be generated on the client side. The solution I came up will be the following: I created a Web API endpoint which uses the "normal" AntiForgeryToken
class to generate the tokens and it will send back the two tokens in the response body and as a cookie. I will render the token with an Angular directive and an interceptor will attach this token as an HTTP header. After that, on the server side, a custom filter will take care of the validation of the tokens.
You can find the solution attached to this article, or you can browse it on GitHub.
The Solution
The server side code will be very simple. There are two cases, if it is the first time in this session that we call this endpoint then we don't have any tokens. The GetTokens
function first parameter is the cookie token, if we already have one, otherwise just provide an empty string
. What is important is that the cookie token will be the same for the lifetime of the session and GetTokens
will provide a null
value if we provide the existing cookie value.
[HttpGet]
[Route("antiforgerytoken")]
public HttpResponseMessage GetAntiForgeryToken()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
HttpCookie cookie = HttpContext.Current.Request.Cookies["xsrf-token"];
string cookieToken;
string formToken;
AntiForgery.GetTokens(cookie == null ? "" : cookie.Value, out cookieToken, out formToken);
AntiForgeryTokenModel content = new AntiForgeryTokenModel
{
AntiForgeryToken = formToken
};
response.Content = new StringContent(
JsonConvert.SerializeObject(content), Encoding.UTF8, "application/json");
if (!string.IsNullOrEmpty(cookieToken))
{
response.Headers.AddCookies(new[]
{
new CookieHeaderValue("xsrf-token", cookieToken)
{
Expires = DateTimeOffset.Now.AddMinutes(10),
Path = "/"
}
});
}
return response;
}
Next is the Angular directive. It will call the endpoint, then render the token as a hidden input element as MVC normally does.
(function() {
'use strict';
function antiForgeryDirectiveController(appService) {
var directive = this;
directive.antiForgeryToken = '';
directive.activate = function () {
appService.getAntiForgeryToken().then(function(data) {
directive.antiForgeryToken = data.antiForgeryToken;;
});
};
directive.activate();
}
function antiForgeryTokenDirective() {
return {
scope: {},
controllerAs: 'directive',
template: '<input id="__antiForgeryToken"
name="antiForgeryToken" type="hidden"
value="{{directive.antiForgeryToken}}" />',
controller: [ 'appService', antiForgeryDirectiveController ]
}
}
angular.module('demoApp').directive('antiforgerytoken', antiForgeryTokenDirective);
})();
We have another job at the client side. If the HTML contains the hidden input element (we can identify it by id), then we need to add an HTTP header to the request. For this purpose, we will use an interceptor:
(function() {
'use strict';
function antiForgeryInterceptor() {
return {
request: function($config) {
var antiForgeryTokenField = document.getElementById('__antiForgeryToken');
if (antiForgeryTokenField) {
var xsrfToken = antiForgeryTokenField.value;
$config.headers['XSRF-TOKEN'] = xsrfToken;
}
return $config;
}
};
}
angular.module('demoApp').service('antiForgeryInterceptor', antiForgeryInterceptor);
})();
The last step is the server side validation:
public sealed class ValidateAntiForgeryTokenFilter : ActionFilterAttribute
{
private const string XsrfHeader = "XSRF-TOKEN";
private const string XsrfCookie = "xsrf-token";
public override void OnActionExecuting(HttpActionContext actionContext)
{
HttpRequestHeaders headers = actionContext.Request.Headers;
IEnumerable xsrfTokenList;
if (!headers.TryGetValues(XsrfHeader, out xsrfTokenList))
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest);
return;
}
string tokenHeaderValue = xsrfTokenList.First();
CookieState tokenCookie = actionContext.Request.Headers.GetCookies().Select(c =>
c[XsrfCookie]).FirstOrDefault();
if (tokenCookie == null)
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest);
return;
}
try
{
AntiForgery.Validate(tokenCookie.Value, tokenHeaderValue);
}
catch (HttpAntiForgeryException)
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest);
}
}
}
After that, we can use this filter as the original ValidateAntyForgeryToken
attribute.
History
- April 15, 2016 - Initial version