71

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?

vittore
  • 17,449
  • 6
  • 44
  • 82
dbruning
  • 5,042
  • 5
  • 34
  • 35
  • 1
    I'm trying to come up with a solution to this as well and wondering if comparing the two cookies is okay when they both can be altered by an attacker? If your salt is discovered then is this not compromised? – BenCr Oct 25 '13 at 11:03
  • BenCr, only javascript running on my domain can read the cookie and put it into the header. So if there was a malicious site which caused the browser to submit a request to my site, the request wouldn't have the header, so it will reject the request. – dbruning Oct 28 '13 at 05:11
  • can you explain what is the result of the solution you have describes here? how does it fail? or are you asking us to find holes in the security? – user1852503 Nov 12 '13 at 01:02
  • Just looking for comment. It doesn't fail (AFAIK) – dbruning Nov 12 '13 at 19:58
  • 2
    for all future users, this is a helpful link in case You are working with [Asp.net MVC and AngularJs](http://techbrij.com/angularjs-antiforgerytoken-asp-net-mvc) – Devesh Nov 13 '13 at 16:40
  • @dbruning: can u please show how do u use this token on client side/angularjs.. what code u need in angular to use this?? – harishr Jul 12 '14 at 16:17
  • @HarishR there's nothing to do in Angular. This post is about the work needed on the server to work with Angular's built-in XSRF behaviour. – dbruning Jul 13 '14 at 22:28
  • your implementation seems flawless all looks good. – dmportella Jul 29 '14 at 21:04
  • Two things - 1) why do you hash the auth token instead of just using a random token generated by a CSPRNG? 2) instead of using an auth attribute, I guess it would make sense to implement this in a delegating handler instead - that way around you can short circuit the request way earlier in the Web API pipeline. – Lasse Christiansen Sep 01 '14 at 06:03
  • @sw_lasse 1) we derive the csrfToken from the authToken so that we can check it's correct when it comes back to the server. If it was just a random number, we would have to remember what we sent out so we could verify it was the same when it came back in. 2) possibly. – dbruning Sep 01 '14 at 19:33
  • It was long time ago, but i noticed that in overriden IsAuthorized method you do not invoke base.IsAuthorized(). Sometimes it can be useful when dealing with roles etc. Wouldn't it be better like `return new CsrfTokenHelper().DoesCsrfTokenMatchAuthToken(csrfToken, authToken) && base.IsAuthorized(context);` ? – el vis Apr 01 '15 at 11:44
  • Well, removing cookies locally at the server with `HttpContext.Current.Response.Cookies.Remove` doesn't make them disappear at the client side. – Wiktor Zychla Apr 22 '16 at 07:33

4 Answers4

8

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

Community
  • 1
  • 1
vittore
  • 17,449
  • 6
  • 44
  • 82
  • 11
    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. – dbruning May 10 '13 at 07:12
  • @dbruning It is just helper generation token, you can use it wherever you want – vittore May 10 '13 at 13:00
  • 1
    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. – dbruning May 13 '13 at 07:00
  • 6
    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. – ccorrin Jul 20 '13 at 22:15
  • 3
    @ccorrin have you followed my link ? there is option for ajax case , you can use it. – vittore Jul 20 '13 at 23:31
0

This solution isn't secure since CSRF attacks are still possible as long as the Auth cookie is valid. Both the auth and the xsrf cookie will be sent to the server when an attacker makes you perform a request via another site, and therefore you are still vulnerable until the user does a "hard" logout.

Each request or session should have its own unique token to truly prevent CRSF attacks. But probably the best solution is to not use cookie based authentication but token based authentication such as OAuth. This prevents other websites from using your cookies to perform unwanted requests, since the tokens are used in http headers instead of cookies. And http headers are not automatically send.

  1. Token Based Authentication using ASP.NET Web API 2, Owin, and Identity
  2. AngularJS Token Authentication using ASP.NET Web API 2, Owin, and Identity

These excellent blog posts contain information of how to implement OAuth for WebAPI. The blog posts also contains great information of how to integrate it with AngularJS.

Another solution might be to disable CORS and only accept incoming requests from whitelisted domains. However this won't work for non-website applications, such as mobile and/or desktop clients. Next to that once your website is vulnerable to a XSS attack the attacker will still be able to forge requests on your behalve.

Dibran
  • 1,435
  • 16
  • 24
  • 3
    That's not correct. The malicious website can't cause the browser to set the X-XSRF-TOKEN *header*. – dbruning Jun 16 '16 at 01:54
  • This seems to be how Angular's CookieXSRFStrategy works: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Protecting_REST_Services:_Use_of_Custom_Request_Headers . I'm going to use this strategy for a REST api. – C.M. Jun 16 '17 at 21:16
-1

I think your code is flawed. The whole idea around prevent CSRF is to prevent a unique token on each REQUEST, not each session. If the anti-forgery token is a session persisted value, the ability to perform CSRF still remains. You need to provide a unique token on each request...

Kolchy
  • 85
  • 1
  • 6
  • 4
    [OWASP says](https://www.owasp.org/index.php/Anti_CSRF_Tokens_ASP.NET#ASP.NET_MVC_and_Web_API:_Anti-CSRF_Token) it's standard to do it per session – dbruning Jun 16 '16 at 01:52
-5

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

dbruning
  • 5,042
  • 5
  • 34
  • 35