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

4

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
{
    //  https://mcmap.net/q/267448/-problems-implementing-validatingantiforgerytoken-attribute-for-web-api-with-mvc-4-rc
    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?

Mona answered 22/3, 2013 at 15:47 Comment(12)
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?Hildagarde
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.Mona
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?Gelb
Just looking for comment. It doesn't fail (AFAIK)Mona
for all future users, this is a helpful link in case You are working with Asp.net MVC and AngularJsIonian
@dbruning: can u please show how do u use this token on client side/angularjs.. what code u need in angular to use this??Pursuer
@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.Mona
your implementation seems flawless all looks good.Jurat
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.Hominid
@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.Mona
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); ?Backstretch
Well, removing cookies locally at the server with HttpContext.Current.Response.Cookies.Remove doesn't make them disappear at the client side.Monoploid
M
-5

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

Mona answered 7/1, 2015 at 4:53 Comment(0)
B
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

Batchelor answered 10/5, 2013 at 1:16 Comment(5)
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.Mona
@Mona It is just helper generation token, you can use it wherever you wantBatchelor
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.Mona
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.Schaerbeek
@Schaerbeek have you followed my link ? there is option for ajax case , you can use it.Batchelor
N
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.

Neille answered 11/4, 2016 at 9:58 Comment(2)
That's not correct. The malicious website can't cause the browser to set the X-XSRF-TOKEN header.Mona
This seems to be how Angular's CookieXSRFStrategy works: owasp.org/index.php/… . I'm going to use this strategy for a REST api.Strafe
C
-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...

Caftan answered 7/4, 2016 at 2:40 Comment(1)
OWASP says it's standard to do it per sessionMona
M
-5

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

Mona answered 7/1, 2015 at 4:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.