Angular against Asp.Net WebApi, implement CSRF on the server

16,134

Solution 1

Haven't had any problems pointed out with the code, so I consider the question answered.

Solution 2

Your code seems to be fine. The only thing is, you don't need most of the code you have as web.api runs "on top" of asp.net mvc, and latter has built in support for anti-forgery tokens.

In comments dbrunning and ccorrin express concerns that you only able to use build in AntiForgery tokens only when you are using MVC html helpers. It is not true. Helpers can just expose session based pair of tokens that you can validate against each other. See below for details.

UPDATE:

There is two methods you can use from AntiForgery:

  • AntiForgery.GetTokens uses two out parameters to return cookie token and form token

  • AntiForgery.Validate(cookieToken, formToken) validates if pair of tokens is valid

You totally can repurpose those two methods and use formToken as headerToken and cookieToken as actual cookieToken. Then just call validate on both within attribute.

Another solution is to use JWT (check eg MembershipReboot implementation)

This link shows how to use built in anti-forgery tokens with ajax:

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

    $.ajax("api/values", {
        type: "post",
        contentType: "application/json",
        data: {  }, // JSON data goes here
        dataType: "json",
        headers: {
            'RequestVerificationToken': '@TokenHeaderValue()'
        }
    });
</script>


void ValidateRequestHeader(HttpRequestMessage request)
{
    string cookieToken = "";
    string formToken = "";

    IEnumerable<string> tokenHeaders;
    if (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);
}

Also take a look at this question AngularJS can't find XSRF-TOKEN cookie

Share:
16,134
dbruning
Author by

dbruning

Updated on June 24, 2022

Comments

  • dbruning
    dbruning almost 2 years

    I'm implementing a website in Angular.js, which is hitting an ASP.NET WebAPI backend.

    Angular.js has some in-built features to help with anti-csrf protection. On each http request, it will look for a cookie called "XSRF-TOKEN" and submit it as a header called "X-XSRF-TOKEN" .

    This relies on the webserver being able to set the XSRF-TOKEN cookie after authenticating the user, and then checking the X-XSRF-TOKEN header for incoming requests.

    The Angular documentation states:

    To take advantage of this, your server needs to set a token in a JavaScript readable session cookie called XSRF-TOKEN on first HTTP GET request. On subsequent non-GET requests the server can verify that the cookie matches X-XSRF-TOKEN HTTP header, and therefore be sure that only JavaScript running on your domain could have read the token. The token must be unique for each user and must be verifiable by the server (to prevent the JavaScript making up its own tokens). We recommend that the token is a digest of your site's authentication cookie with salt for added security.

    I couldn't find any good examples of this for ASP.NET WebAPI, so I've rolled my own with help from various sources. My question is - can anyone see anything wrong with the code?

    First I defined a simple helper class:

    public class CsrfTokenHelper
    {
        const string ConstantSalt = "<ARandomString>";
    
        public string GenerateCsrfTokenFromAuthToken(string authToken)
        {
            return GenerateCookieFriendlyHash(authToken);
        }
    
        public bool DoesCsrfTokenMatchAuthToken(string csrfToken, string authToken) 
        {
            return csrfToken == GenerateCookieFriendlyHash(authToken);
        }
    
        private static string GenerateCookieFriendlyHash(string authToken)
        {
            using (var sha = SHA256.Create())
            {
                var computedHash = sha.ComputeHash(Encoding.Unicode.GetBytes(authToken + ConstantSalt));
                var cookieFriendlyHash = HttpServerUtility.UrlTokenEncode(computedHash);
                return cookieFriendlyHash;
            }
        }
    }
    

    Then I have the following method in my authorisation controller, and I call it after I call FormsAuthentication.SetAuthCookie():

        // http://www.asp.net/web-api/overview/security/preventing-cross-site-request-forgery-(csrf)-attacks
        // http://docs.angularjs.org/api/ng.$http
        private void SetCsrfCookie()
        {
            var authCookie = HttpContext.Current.Response.Cookies.Get(".ASPXAUTH");
            Debug.Assert(authCookie != null, "authCookie != null");
            var csrfToken = new CsrfTokenHelper().GenerateCsrfTokenFromAuthToken(authCookie.Value);
            var csrfCookie = new HttpCookie("XSRF-TOKEN", csrfToken) {HttpOnly = false};
            HttpContext.Current.Response.Cookies.Add(csrfCookie);
        }
    

    Then I have a custom attribute which I can add to controllers to make them check the csrf header:

    public class CheckCsrfHeaderAttribute : AuthorizeAttribute
    {
        //  http://stackoverflow.com/questions/11725988/problems-implementing-validatingantiforgerytoken-attribute-for-web-api-with-mvc
        protected override bool IsAuthorized(HttpActionContext context)
        {
            // get auth token from cookie
            var authCookie = HttpContext.Current.Request.Cookies[".ASPXAUTH"];
            if (authCookie == null) return false;
            var authToken = authCookie.Value;
    
            // get csrf token from header
            var csrfToken = context.Request.Headers.GetValues("X-XSRF-TOKEN").FirstOrDefault();
            if (String.IsNullOrEmpty(csrfToken)) return false;
    
            // Verify that csrf token was generated from auth token
            // Since the csrf token should have gone out as a cookie, only our site should have been able to get it (via javascript) and return it in a header. 
            // This proves that our site made the request.
            return new CsrfTokenHelper().DoesCsrfTokenMatchAuthToken(csrfToken, authToken);
        }
    }
    

    Lastly, I clear the Csrf token when the user logs out:

    HttpContext.Current.Response.Cookies.Remove("XSRF-TOKEN");
    

    Can anyone spot any obvious (or not-so-obvious) problems with that approach?

  • dbruning
    dbruning almost 11 years
    The anti-forgery support in asp.net mvc relies on using mvc to generate your html, so that it can put the request verification token into your HTML forms as a hidden field. I'm not using mvc hence my html forms don't have that token.
  • vittore
    vittore almost 11 years
    @dbruning It is just helper generation token, you can use it wherever you want
  • dbruning
    dbruning almost 11 years
    Maybe. I don't remember the exact details, but I couldn't find a clean way to just ask for the csrf cookie. The built-in AntiForgery methods seem to want to work with forms, whereas I'm just working with POST'ed JSON data. If you can share a clean way to get the csrf cookie, that could replace my CsrfTokenHelper class above. You still would need a nice way to set the cookie on the outgoing request & check the header on the incoming request.
  • ccorrin
    ccorrin almost 11 years
    For people not wanting to use MVC for their views, the MVC helpers are not an option. Alot of people want to keep their client-side code pure HTML/JS to take advantage of multiple platforms, and using tools such as phonegap. If your views are in razor your limited in that regard.
  • vittore
    vittore almost 11 years
    @ccorrin have you followed my link ? there is option for ajax case , you can use it.
  • dbruning
    dbruning almost 8 years
    OWASP says it's standard to do it per session
  • dbruning
    dbruning almost 8 years
    That's not correct. The malicious website can't cause the browser to set the X-XSRF-TOKEN header.
  • C.M.
    C.M. almost 7 years
    This seems to be how Angular's CookieXSRFStrategy works: owasp.org/index.php/… . I'm going to use this strategy for a REST api.