There are broad scenarios in which a single page app (SPA) needs to handle authentication:
- The SPA is using an API custom built for the app itself — the API being used is not public in any other way
- The SPA is using an API built for public consumption — one with a full fledged authentication system of its own
Unpopular Advice About Purpose Built APIs
For single page apps that use a purpose built API, I’d suggest avoiding dealing with authentication at all. Let your backend framework of choice maintain a login page and a session.
I say this is unpopular advice because it’s not fancy. Sessions have been around forever. They are well understood, distinctly boring, and probably the best choice anyway.
Seriously, if an SPA is using a purpose built API, just let the backend maintain a login page + session.
Single Page Apps Built with a Public API
Public can mean a lot of things in this case, but let’s assume that it just means the API has its own authentication system so the SPA itself can consume as well as other clients. Hopefully this authentication system is something standard like OAuth.
In these cases the authorization server will issue some sort of token to accompany and authorize subsequent requests to the resource server.
These terms are borrowed from the OAuth spec:
- Authorization Server: the server that issues authentication tokens (access and/or refresh tokens in the case of oauth 2)
- Resource Server: the api itself, a set of endpoints or resources whose authentication is handled by the tokens issued by the authorization server
That token, when it’s received from the authorization server, should always be stored in memory scoped to your application. Don’t put it in the global window
scope. Don’t set it in local storage or in a cookie. All of that is (or could be) vulnerable to cross site request forgery attacks.
I’m used scoped here a bit loosely. It might mean the token goes into your redux store or it may mean it’s just scoped to a closure.
(function (document) { let accessToken = null; if (document.location.pathname === '/oauth-callback') { accessToken = extractAccessTokenFromHash(document.location.hash); navigateTo('/somewhereElse'); } }(document));
That token can then be pulled out of the application state to make requests to the resource server.
An Example with OAuth 2
I recently had to built my own OAuth 2 authorization server (thanks to the PHP League for the help) and accompanying resource server.
This story has three hosts:
- login.{app}.com the oauth authorization server (this issues access tokens)
- api.{app}.com the resource server (the protect API)
- {app}.com the single page app
My usual approach for SPAs has been to just let the backend maintain a session (see above). In this case, that wasn’t going to work. The API needed to be available to multiple clients.
To make that work, I allowed only the single page app’s OAuth 2 Client to use an implicit grant. This flow redirects the user after a login with the access token but no refresh token.
On redirect, I stored the access token in my SPA’s redux state. This has a downside: whenever the user refreshes the page the entirety of the state is lost including the access token.
To work around that, the authorization server (at login.{app}.com
) maintains a session for a day. Should the user refresh the SPA ({app}.com
), they’ll get thrown into the oauth dance again. Since they’ve already logged into the authorization server, OAuth 2 redirect is quick and requires no additional interaction from them — they don’t have to login again or approve the app again. A new access token is issued, the old access token is lost, and the user is redirected to the page on which they were before. The old access token is collected by automated expiration processes down the road when it actually expires.
Another important thing to note is the access tokens issued for the SPA have a short TTL. If they are leaked or otherwise compromised, they are only valid for a short time.