Search code examples
javascriptphpcookiescsrfjwt

Secure ways to implement CSRF free JWT authentification


I have just learned about JWT for authentification. Storing the JWT token in localStorage/sessionStorage is exposed to XSS. Storing it in a cookie is vulnerable to CSRF. I have been researching this and I thought of this solution, but I'm not sure how secure is this and if I should be using it in production.

The soulution is to get the JWT token from server, store it in cookie. Generate a CSRF token (that will be stored in JWT) that will be sent with each HTML page, either in a hidden HTML field of as a global JS variable (). That CSRF token will be sent with every request using JS/AJAX. This way we can rule out CSRF first then verify the JWT token.

I'm not sure whether a new token should be sent with each loaded page, or a single token should be held per session. The 1st case would mean that only the last loaded page/form would be able to submit (which could be problematic if the user is having multiple tabs of other pages open).

Is this a secure solution that might be used in production ?

Also, what other solutions would be viable to reach the same goal ?


Solution

  • Here is my understanding of the issue.

    • JWT Token - stored in httponly/secure cookie - this ensures any Javascript does not have access to it
    • CSRF Token - stored by JS - on login, attach the claims as a header (including your CSRF value)

    When the JS gets the header, it can store the claim in a cookie, localstorage, or session storage. Because no JS has access to the JWT token in the cookie, by attaching the CSRF with every request and making sure it matches what's in the JWT, you're ensuring that it's your JS sending the request and that it was your backend that issued the token.

    A hidden field isn't a great solution because you'll have to add it to each request. Depending on what framework you're using, you should be able to load the CSRF token into whatever is responsible for sending the token as a header on each request when the page is refreshed. The benefit to having all the claims in local storage or a cookie is you can preserve the users front end state. Having the exp claim means you can tell when the user no longer has a valid token in the secure jwt token cookie and you can redirect to a login page.

    • sessionstorage - is specific to the tab only
    • localstorage - is specific to the domain across tabs
    • cookie - is specific to the domain across tabs

    Add CSFR to AJAX request:

        $.ajax({
            type:"POST",
            beforeSend: function (request)
            {
                request.setRequestHeader("CSRF-TOKEN", csrfToken);
            },
            url: "entities",
            data: "json=" + escape(JSON.stringify(createRequestObject)),
            processData: false,
            success: function(msg) {
                $("#results").append("The result =" + StringifyPretty(msg));
            }
    });
    

    Even though you're not using Angular, this still applies. You should attach the token as a header with the JS and it's fine to get that value from a cookie.

    From the Angular JS Documentation

    When performing XHR requests, the $http service reads a token from a cookie (by default, XSRF-TOKEN) and sets it as an HTTP header (X-XSRF-TOKEN). Since only JavaScript that runs on your domain can read the cookie, your server can be assured that the XHR came from JavaScript running on your domain.