Authenticating an SPA: What to do About the Developer Console
In a single page app, all of the decisions about what view/subview to render occurs on the client. This means that ideally the client would be able to authenticate the currently logged in user on transitions to sensitive pages and access its data without going back to the server. This means that special care needs to be made to protect our application from a malicious user interacting with the developer console present in all modern browsers. One possible security vulnerability is the escalation of a globally stored user user role. This would cause the hacker to view a part of the website that they were forbidden to.
This blog post summarizes my attempts at adding an additional layer of security to my locally stored authentication information. Also, I just want to make it clear: even if a perfect solution is found for this vulnerability, server endpoints still need to verify the request. The client can never be trusted and performing crypto on the browser is a bad idea.
No one is an island
Our problem is simply stated: we need a way to store the user authentication information on the client in a trustworthy manner. If we allow ourselves one connection to the client, we can authenticate the credentials and save a copy of the authentication information in a way that prevents it from being easily changed by the console. This means we have moved the crypto part off of the client and we can begin to think about trusting the data that we have stored.
More Callbacks? I prefer working with raw data types
Sometimes, it's not convinient to authenticate against a closure. For instance, it might make more sense to store your authentication information in a redux store so that you can keep all of your data in a particular place. While this is probably unnecessary in all but the extreme cases, it is possible to prevent the globally available data store from being tampered with. We can let them modify the data so long as we have some way of invalidating it afterwards and fetching new data to use.
Persisting Authentication Between Browser Sessions
In most cases, in order to achieve persistance, an additional storage mechanism is required whose lifetime exceeds the memory stack of the browser window to store the session data. Two options exist in the mainstream: storing a session token that is referenced agains the server, or an encrypted version of JSON called a JWT.
Choose your weapon: JWT vs Session Token and Local Storage vs Cookies
JWTs are a good storage candidate because they allow for the client to be responsible for keeping track of the permissions of the currently logged in user. This removes the need for a session store in most cases which dramatically increases scalability and there are no more potential problem of synchronizing the store among processes with separate memory. However JWTs require a secret key to be decrypted which means it can't happen on the frontend with the same key that the server uses, say for its csrf protection. A malicious visiter would be able to download the source code compiled on a few different views and look for similar strings. One of them would be the secret key so its easily brute-forcible.
Now that we have decided to use JWTs, we need to figure out where to store them. The two most common approaches are cookies or local storage. For a good comparison of the two for storing JWTs see this blog post. In short, local storage is susceptible to xss and cookies are susceptible to csrf. Since csrf protection is standard in most backend and xss is a bit tricker, I opted to store my JWT in a cookie with the HttpOnly, and Secure flags
Handling the Request for Authentication Information
Since we rely on the server to decrypt the JWT, we are always forced to make a single request when the client starts that authenticates the token. In order to avoid race conditions, it is reccomended that this request is made synchronously, before the application starts.
In this blog post I described a technique I use to secure my authentication information on the client in a read only manner. This can be used to perform authentication logic on the client and prevent unnecessary requests to the backend. Even while this is the case, it is necessary to authenticate the backend endpoints to prevent data leakage. If you have any questions, comments, or can think of a way to solve this in the isomorphic environment, please leave a comment.