78

We have some existing MVC web services that are called AJAX style from web pages. These services make use of the ValidateAntiForgeryToken attribute to help prevent request forgeries.

We are looking to migrate these services to Web API, but there appears to be no equivalent anti-forgery functionality.

Am I missing something? Is there a different approach to addressing request forgeries with Web API?

ScottS
  • 8,455
  • 3
  • 30
  • 50
  • 3
    While Darin's answer is correct, DazWilkin lead us to be better solution of putting the token into the headers. http://stackoverflow.com/questions/11725988/problems-implementing-validatingantiforgerytoken-attribute-for-web-api-with-mvc/11726560#11726560 – ScottS Aug 03 '12 at 22:41
  • 8
    That better solution is also from Darin :) – Peter Porfy Apr 12 '13 at 14:43

6 Answers6

58

You could implement such authorization attribute:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
{
    public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
    {
        try
        {
            AntiForgery.Validate();
        }
        catch
        {
            actionContext.Response = new HttpResponseMessage 
            { 
                StatusCode = HttpStatusCode.Forbidden, 
                RequestMessage = actionContext.ControllerContext.Request 
            };
            return FromResult(actionContext.Response);
        }
        return continuation();
    }

    private Task<HttpResponseMessage> FromResult(HttpResponseMessage result)
    {
        var source = new TaskCompletionSource<HttpResponseMessage>();
        source.SetResult(result);
        return source.Task;
    }
}

and then decorate your API actions with it:

[ValidateAntiForgeryToken]
public HttpResponseMessage Post()
{
    // some work
    return Request.CreateResponse(HttpStatusCode.Accepted);
}
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • Thanks, System.Web.Helpers.AntiForgery looks like it will be the answer to my problem. – ScottS Jul 14 '12 at 21:42
  • in my case, data comes as a JSON string via the following code, and the above solution didn't work: $.ajax({ url: url, method: "PUT", contentType: "application/json", dataType: "json", data: formJsonData }) – Tohid May 21 '15 at 18:51
  • 3
    I think this approach will only work if you post to your ajax service using form encoding and explicitly include the token in the ajax request form data. If you want to use JSON for your ajax data, you will need to roll-your-own code to extract the tokens and call the two-parameter overload of AntiForgery.Validate. you can either send the "formToken" parameter in the json data or in an HTTP header like this http://stephenwalther.com/archive/2013/03/05/security-issues-with-single-page-apps – Andy Jul 07 '16 at 13:22
  • Hi, @Darin, could you, please, explain why `ValidateAntiForgeryTokenAttribute` implements `IAuthorizationFilter`, instead of `IAuthenticationFilter`? – Gerardo Lima Jul 11 '17 at 15:22
  • 2
    @Darin Am I right that it's not possible to have AntiForgery without referencing `System.Web.WebPages`? Because I really don't want to my WebAPI library being referencing `WebPages`... – Alex Zhukovskiy Jul 31 '17 at 16:14
  • @GerardoLima using `IAuthenticationFilter` makes no sense because authentication is about logging in. **Authorization** is what we're doing here. – Rudey Dec 29 '17 at 16:26
  • @AlexZhukovskiy There's nothing here that references `System.Web.WebPages`. – Rudey Dec 29 '17 at 21:06
  • @RuudLenders sorry for musinderstanding, I meant `MVC` stack. It's weird to have both `Http` and `Mvc` references in your project. You get multiple conflicts from having similar types in different namespaces. I believe this problem has been solved in ASP.Net core apps, but it's not easy to migrate. – Alex Zhukovskiy Dec 30 '17 at 19:14
  • @AlexZhukovskiy I don't believe anything references `Mvc` either. Are you referencing `System.Web.Mvc.IAuthorizationFilter`? You should be referencing `System.Web.Http.Filters.IAuthorizationFilter` instead. – Rudey Dec 31 '17 at 10:26
  • 1
    @RuudLenders `AntiForgery` is located in `Microsoft.AspNet.WebPages`, which is obviosly depended on `WebPages` and `System.Web.Razor`. However, it's not possible to do it with API because in this case there is no possibility to inject antiforgery token on the page because there is no page. – Alex Zhukovskiy Jan 01 '18 at 11:30
  • @Darin Dimitrov - thanks for the answer. How can I use this code when calling the wen api from independent angular 7 app? not using Razor or something like this. – Batsheva Dec 19 '19 at 08:10
23

Complementing Above code FilterAttribute

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
    {
        public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            try
            {
                string cookieToken = "";
                string formToken = "";

                IEnumerable<string> tokenHeaders;
                if (actionContext.Request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders))
                {
                    string[] tokens = tokenHeaders.First().Split(':');
                    if (tokens.Length == 2)
                    {
                        cookieToken = tokens[0].Trim();
                        formToken = tokens[1].Trim();
                    }
                }
                AntiForgery.Validate(cookieToken, formToken);
            }
            catch (System.Web.Mvc.HttpAntiForgeryException e)
            {
                actionContext.Response = new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.Forbidden,
                    RequestMessage = actionContext.ControllerContext.Request
                };
                return FromResult(actionContext.Response);
            }
            return continuation();
        }

        private Task<HttpResponseMessage> FromResult(HttpResponseMessage result)
        {
            var source = new TaskCompletionSource<HttpResponseMessage>();
            source.SetResult(result);
            return source.Task;
        }

Html Function Using Razor

@functions{
    public string TokenHeaderValue()
        {
            string cookieToken, formToken;
            AntiForgery.GetTokens(null, out cookieToken, out formToken);
            return cookieToken + ":" + formToken;
        }
}

Using Angular

return $http({
   method: 'POST',
   url: '@Url.Content("~/api/invite/")',
   data: {},
   headers: {
       'RequestVerificationToken': '@TokenHeaderValue()'
   }
});
Oswaldo Alvarez
  • 4,772
  • 1
  • 22
  • 20
  • 9
    you should at least giver reference. The orignal article is here http://www.asp.net/web-api/overview/security/preventing-cross-site-request-forgery-(csrf)-attacks – Idrees Khan Nov 02 '14 at 08:13
  • Why is this better than the answer? – Ian Warburton May 11 '15 at 00:05
  • 6
    I don't believe this works (and nor does the original you copied from). The cookieToken must be sent as a httpOnly cookie so that it can't be manipulated by the client-side script. By simply joining the two tokens together to make one, you're allowing JavaScript to manipulate both the tokens, so you're defeating the whole object of this mechanism. – Andy Jul 07 '16 at 12:38
  • 1
    Hi, @Oswaldo, could you, please, explain why `ValidateAntiForgeryTokenAttribute` implements `IAuthorizationFilter`, instead of `IAuthenticationFilter`? – Gerardo Lima Jul 11 '17 at 15:23
  • 1
    @Andy the question is _whose_ javascript? This approach relies on the third-party evil website's javascript not being able to access the cookie, while the protected website's javascript can access the cookie. An XSS vulnerability on the protected website's page would defeat this by allowing an attacker to gain access to the cookie. So protecting against XSS is key to this approach. – Johann Oct 29 '20 at 16:24
  • Thanks for this answer, it has everything that I needed – Alexander Dec 03 '20 at 16:20
  • The working link to original article (although without filter implementation) that @IdreesKhan mentioned is https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/preventing-cross-site-request-forgery-csrf-attacks – Mariusz Pawelski Feb 17 '21 at 12:57
6

This link helped, you can retrieve the anti-forgery token from the razor view and pass the token as a header:

var csrfToken = $("input[name='__RequestVerificationToken']").val(); 
$.ajax({
    headers: { __RequestVerificationToken: csrfToken },
    type: "POST",
    dataType: "json",
    contentType: 'application/json; charset=utf-8',
    url: "/api/products",
    data: JSON.stringify({ name: "Milk", price: 2.33 }),
    statusCode: {
        200: function () {
            alert("Success!");
        }
    }
});
Adrian
  • 7,745
  • 5
  • 28
  • 28
5

Oswaldo's answer but implemented as an AuthorizeAttribute

  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
  public class ApiValidateAntiForgeryToken : AuthorizeAttribute
  {
    public static string GenerateAntiForgeryTokenForHeader() {
      string cookieToken, formToken;
      AntiForgery.GetTokens(null, out cookieToken, out formToken);
      return cookieToken + ":" + formToken;
    }


    protected override bool IsAuthorized(HttpActionContext actionContext) {
      var headers = actionContext.Request.Headers;

      // we pass both the cookie and the form token into a single header field
      string headerToken = headers.Contains("__RequestVerificationToken") ? headers.GetValues("__RequestVerificationToken").FirstOrDefault() : null;

      if (headerToken == null) {
        return false;
      }

      string[] tokens = headerToken.Split(':');
      if (tokens.Length != 2) {
        return false;
      }

      string cookieToken = tokens[0].Trim();
      string formToken = tokens[1].Trim();

      try {
        AntiForgery.Validate(cookieToken, formToken);
      }
      catch {
        return false;
      }

      return base.IsAuthorized(actionContext);
    }
  }

You can decorate your controller or methods with [ApiValidateAntiForgeryToken] and then pass RequestVerificationToken: "@ApiValidateAntiForgeryToken.GenerateAntiForgeryTokenForHeader()" as a header for the method in your razor javascript code.

Javier G.
  • 937
  • 1
  • 10
  • 10
  • Hi, @Javier, could you, please, explain why your implementation of `ApiValidateAntiForgeryToken ` inherits from `AuthorizeAttribute`, instead of implementing `IAuthorizationFilter`? – Gerardo Lima Jul 11 '17 at 15:26
  • 1
    Hi Gerardo, basically it is the same thing but implemented in a different way. See https://stackoverflow.com/questions/27021506/what-is-the-difference-between-using-authorizeattribute-or-iauthorizationfilter - In my case I needed it for a project that required an AutorizeAttribute and didn't work with the interface (I think it was because of SignalR, but I can't remember anymore). In any case please rather consider the other answer I gave in this very same post, since it is more secure. – Javier G. Jul 13 '17 at 17:57
5

After thinking about this some more, it is a bad idea to mix the cookie and the form tokens since it defeats the whole purpose of the anti forgery token. It is better to keep the cookie part as a cookie while moving the form part to an auth header, therefore this new answer (again as an AuthorizeAttribute).

using System;
using System.Linq;
using System.Net.Http;
using System.Web;
using System.Web.Helpers;
using System.Web.Http;
using System.Web.Http.Controllers;

  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
  public class ApiValidateAntiForgeryToken : AuthorizeAttribute {
    public const string HeaderName = "X-RequestVerificationToken";

    private static string CookieName => AntiForgeryConfig.CookieName;

    public static string GenerateAntiForgeryTokenForHeader(HttpContext httpContext) {
      if (httpContext == null) {
        throw new ArgumentNullException(nameof(httpContext));
      }

      // check that if the cookie is set to require ssl then we must be using it
      if (AntiForgeryConfig.RequireSsl && !httpContext.Request.IsSecureConnection) {
        throw new InvalidOperationException("Cannot generate an Anti Forgery Token for a non secure context");
      }

      // try to find the old cookie token
      string oldCookieToken = null;
      try {
        var token = httpContext.Request.Cookies[CookieName];
        if (!string.IsNullOrEmpty(token?.Value)) {
          oldCookieToken = token.Value;
        }
      }
      catch {
        // do nothing
      }

      string cookieToken, formToken;
      AntiForgery.GetTokens(oldCookieToken, out cookieToken, out formToken);

      // set the cookie on the response if we got a new one
      if (cookieToken != null) {
        var cookie = new HttpCookie(CookieName, cookieToken) {
          HttpOnly = true,
        };
        // note: don't set it directly since the default value is automatically populated from the <httpCookies> config element
        if (AntiForgeryConfig.RequireSsl) {
          cookie.Secure = AntiForgeryConfig.RequireSsl;
        }
        httpContext.Response.Cookies.Set(cookie);
      }

      return formToken;
    }


    protected override bool IsAuthorized(HttpActionContext actionContext) {
      if (HttpContext.Current == null) {
        // we need a context to be able to use AntiForgery
        return false;
      }

      var headers = actionContext.Request.Headers;
      var cookies = headers.GetCookies();

      // check that if the cookie is set to require ssl then we must honor it
      if (AntiForgeryConfig.RequireSsl && !HttpContext.Current.Request.IsSecureConnection) {
        return false;
      }

      try {
        string cookieToken = cookies.Select(c => c[CookieName]).FirstOrDefault()?.Value?.Trim(); // this throws if the cookie does not exist
        string formToken = headers.GetValues(HeaderName).FirstOrDefault()?.Trim();

        if (string.IsNullOrEmpty(cookieToken) || string.IsNullOrEmpty(formToken)) {
          return false;
        }

        AntiForgery.Validate(cookieToken, formToken);
        return base.IsAuthorized(actionContext);
      }
      catch {
        return false;
      }
    }
  }

Then just decorate your controller or methods with [ApiValidateAntiForgeryToken]

And add to the razor file this to generate your token for javascript:

<script>
var antiForgeryToken = '@ApiValidateAntiForgeryToken.GenerateAntiForgeryTokenForHeader(HttpContext.Current)';
// your code here that uses such token, basically setting it as a 'X-RequestVerificationToken' header for any AJAX calls
</script>
Javier G.
  • 937
  • 1
  • 10
  • 10
0

how you are handling the Antiforgery token implementation for non UI clients? (bot UI and non UI clients calli why does non UI clients needs to pass Antiforgery token? (there is no Antiforgery issue for Non UI clients...)

Venkat Naidu
  • 346
  • 3
  • 11