For single-page apps I use two session tokens. One in localStorage (csrf_token), one in a secure, HttpOnly cookie (auth_token). Both tokens are required for the API to authenticate a request.
Could you explain your reasoning behind the two tokens approach? I'm guessing the HTTPOnly cookie is there to prevent token stealing (low risk as described in the article) and add defence in depth against local storage/cookie zero days?
That is what HTTPOnly is intended for: if the client-side code doesn't need to know the value then it shouldn't see the value. It doesn't make a lot of difference, but it make some difference worth having (security in depth, and all that) by blocking session stealing by malicious code that is somehow injected into your application's client-side payload.
Of course the value is still sent over the wire so is vulnerable to MiTM attacks that are not otherwise mitigated.
I can't think of a benefit off the top of my head for having a second token that is accessible client-side, presumably that is something application specific. Perhaps there is a short-cut to getting a new session token after server-side session expiry (or the user accidentally closing their browser) which can only be used in the presence of a valid client-side token (though shortcuts like that are security holes waiting to happen IMO).
Just to note that the session can still be thoroughly hijacked through malicious javascript on a page protected by HTTPOnly cookies in that the malicious code can make AJAX requests in the users browser to your domain and the HTTPOnly cookies will automatically authorize them. The difference of HTTPOnly cookies vs Local Storage is that the hijacked session is limited to the users browser/computer in the HTTPOnly scenario and in the Local Storage scenario the token can be downloaded and used later by the attacker (this is somewhat mitigated by things like JWT expirations).
Sure. My reasoning: The localStorage token helps protect against CSRF, e.g., this token will not be submitted via a form embedded on another site (cookie will), therefore request will not be authenticated. In an XSS attack (aka "we're screwed"), the secure, HTTPOnly cookie at least adds an additional level of complexity above simply reading a single plaintext localStorage item. Perhaps this is naive? Of course I also take every precaution to prevent XSS from happening at all. Relevant[1].