0

If I were to implement a common OpenID Connection pattern on a SPA, I might have the following relationship:

Auth server <-----------> Client (browser) <-----------> App API server

The user would be redirected to the auth server to login, and an HTTP-Only cookie is set on the auth server with the user's ID token (whose payload contains user details and is signed by a secret) and auth token. Then the auth server redirects the user back to the app with the tokens in the hash string, so they can be persisted.

The hash would have to be set by having the API return something like:

res.send(`<script>
  location.replace('${callbackUrl}#access_token=${accessToken}&id_token=${idToken}')
</script>`)

rather than a HTTP redirect with the token in the query string (because that would probably be logged), and the client-side app can use history.replaceState() to wipe the tokens from browser history. The browser strips these tokens from the hash so the user won't accidentally bookmark or share this URL containing the tokens, then it persists the tokens to localStorage.

I gathered from this article on Stormpath that localStorage isn't the greatest of ideas, since it's vulnerable to XSS (any arbitrarily executed JS living on the same page can read the token from localStorage).

So they advocate for using HTTP-Only cookies to store tokens instead. In this approach, the auth API redirects the user back to the app with headers to set HTTP-Only cookies with:

  • access_token
  • id_token
  • csrf_token

These cookies are set on the auth API server. The UI would also need to know the value of the csrf_token. If the authentication happened with an AJAX request, we could send it in a header; in the case of OpenID, where the app redirected to the API domain, we'd have to use the script approach and send it in the hash for the app UI to receive it. As I understand it, these approaches are roughly equivalent in terms of security -- whether read from the hash or from a header, either way the browser must store this CSRF token to send it in subsequent requests.

Angular's HTTP service handles the former approach (header) by grabbing the token and saving it to a cookie on the UI domain. Then it automatically sends requests to the auth API with the X-XSRF-Token set to that value it has stored in the cookie. Then the API can confirm that this given token matches the one in its csrf_token cookie and determine if the user submitted this request.

This is considerably more complicated than the localStorage approach. The article proposes that this is more secure because localStorage is vulnerable to XSS -- an attacker embedding malicious JS on a page on the UI's domain can steal the localStorage values. (Obviously XSS can be mitigated, but there are certainly situations that arise where we could be vulnerable to XSS, such as a CDN the UI uses getting hacked and sending malicious scripts, or simply a npm package that appears benign and is used in the UI but secretly captures localStorage in the background.)

But how is this CSRF approach any more secure? After all, the CSRF token must be stored in a cookie on the UI, and it can't be a HTTP-only cookie, since requests to the API must include the token in a header. So wouldn't this mean the same XSS vulnerability? The malicious script could read the CSRF cookie on the UI and start sending requests to the API using it.

Am I missing something here? How is the CSRF cookie more secure than putting a token in localStorage, if in either case, the primary means of authenticating with the auth API can be read from the client?

2
  • The only solution I can think of is for the ui server to have one non-UI endpoint that resides on the same domain. Auth API sends POST to ui.example.com/authorized with the CSRF in the payload as well as some shared secret. /authorized endpoint must verify the secret, set a HTTP-only cookie, then redirect to the UI home page. Then, sending AJAX requests withCredentials would send the HTTP-only cookie. Doing this, though, would require the UI to run a server instead of, say, just being hosted on S3 or other static provider.
    – M Miller
    Nov 30, 2017 at 18:57
  • In general the xsrf token is in addition to any authorization.
    – ste-fu
    Nov 30, 2017 at 19:04

1 Answer 1

1

As per the OWASP cheat sheet https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet None of the prevention techniques are effective if you have a XSS vulnerability in your site.

7
  • Would that literally mean that there is zero purpose to implementing the complexities of CSRF over just using localStorage?
    – M Miller
    Nov 30, 2017 at 20:06
  • 1
    @MMiller Not at all - CSRF is still a risk to mitigate when you don't have XSS vulnerabiltiies in your site
    – ste-fu
    Dec 1, 2017 at 10:31
  • @MMiller Also I think that localStorage would be vulnerable to a generic script that could just exfiltrate all the data including your access token and crsf token, but the http only cookie would require more effort from a would-be attacker
    – ste-fu
    Dec 1, 2017 at 10:41
  • From my understanding, CSRF is a risk that only applies to sending requests that use cookie authentication -- so if your app were to only authenticate requests using Authorization headers and tokens coming from localStorage, deciding instead to use HTTP-only cookies and forgo localStorage won't really help, right? Because LS is only vulnerable to XSS and not CSRF, and HTTP-only cookies are only vulnerable to CSRF and not XSS.
    – M Miller
    Dec 3, 2017 at 17:59
  • If your app does graceful degradation with, for example, JS-disabled support, you'd need to use cookies for authentication and hence need to implement CSRF; but if you only send requests using AJAX and a token stored in LS, it's really not any better to use cookies, right? Because you'll still be equally vulnerable to the only thing you were vulnerable to with LS, which is XSS. I believe the Stormpath article is wrong in saying it's less secure to store tokens in LS than it is to use HTTP-only cookies.
    – M Miller
    Dec 3, 2017 at 18:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .