Securing your Single-Page application Anno 2019

Introduction

In this blog post, we will take a look at how we can securely authorize and authenticate our users in a Single-Page application (SPA). We’re specifically talking about browser-based JavaScript applications running on the same domain as the API.

First we had cookies, then we had tokens and now it looks like we’re back to cookies.

Let’s take a closer look to the options and zoom in on the relevant parts.

The options

The major options for giving a user access to our web-based resources are:

  • Cookie based authentication
  • Token based authorization / authentication
  • Federated / delegated authentication (SSO)
  • Windows authentication (Kerberos / NTLM)

Cookies and tokens will be explained further down the post. Along the road we will also talk about federated or delegated authentication in Single Sign-On (SSO). Because I have a mixed feeling about Windows authentication, let me just pretend it doesn’t exist. One thing though: if you can avoid it, please do, consider Azure AD or AD FS as an alternative.

Cookie based authentication

We all know cookies by now, it are those mystical things that live in your browser every website warns you about. A cookie can be created by the server or JavaScript client and contains information that your browser sends to the server on every request.

Regarding authentication, this cookie is created by the server at user logon and removed by the server at user logout. For additional security, two flags are set on the cookie: the http-only flag, telling the browser that the cookie can’t be read by JavaScript and the secure flag, telling the browser that it should only send the cookie over a secure https:// connection (and not as plain text over http://).

The information inside the cookie is encrypted by a secret only known by the server.

Session identifier based cookie

In this case the cookie only contains a unique identifier that refers to a user or account record in the database. For each request the browser makes to the server, the server will lookup the user identity based on the identifier in the cookie.

Claim based cookie

A claim based cookie contains all the claims the server knows about the authenticated user. This way, for each request the browser makes to the server, no database lookup is needed as the user identity can be reconstructed from the information in the cookie.

Just like with stateless tokens, you need some kind of way to replace the cookie in case one of the claims changes. With cookies this could be done on every request as the server can decide when to replace the cookie.

Limitations

Token based authentication

The older SAML 2.0 and WS-Federation protocols are token based but are only suitable in federated login scenario’s: the token is only used once as proof of identity at which time it gets exchanged by a different authentication scheme.

The more popular token based protocols like OAuth2 and OpenID Connect are different in that regard as their token can be reused multiple times to access protected API resources. This said, OAuth2 and OpenID Connect also allow us to only use them as federated login (think about the login with Facebook or Google options on many third-party sites).

OAuth2

OAuth2 is an industry standard protocol for authorization. It is built for web applications, desktop applications, mobile phones and living room devices.

OAuth2 supports multiple grant types but only the implicit grant was deemed suitable for browser-based SPAs. It works by sending a short-lived access token to the SPA which authorizes the SPA to access protected API resources, mostly on behalf of the logged-in user (as most systems link an actual user to the access token).

Just like with session cookies, a server-side lookup is needed on every request in order to identity the user based on the access token.

OpenID Connect

OpenID Connect is an identity layer on top of OAuth2. Many external login providers doing OAuth2, like Facebook, Google, LinkedIn, etc already provided a way to exchange their access token for additional user information by calling some kind of user-info endpoint which was very specific to the provider.

OpenID Connect has standardized the user-info endpoint and also makes it possible to receive an id token (JWT) in addition to the access token we know from OAuth2. Apart from the user-info endpoint, it also standardized scopes, endpoint discovery, dynamic client registration and sessions management.

OAuth2 allows us to use an API, OpenID Connect allows us to authenticate a user

What’s wrong with tokens in SPAs?

Not as secure as promised

  • Token leaking is real. Because tokens are sent as hash-fragment in the URL, they appear in browser history and eventually in logs
  • JWTs have had their own set of issues and stupidities
  • OAuth2 and OpenID Connect is vulnerable to Cross-Site Scripting (XSS) which is much harder to circumvent than Cross-Site Request Forgery in the event of cookie based authentication

Short lifetime

As it’s hard to protect the token in JavaScript land, the advice is to use a very short lifetime for the access token which requires the SPA to restart the implicit flow every, let’s say, 30 minutes. This needs some iframe trickery (silent renew) or in the simplest case, a visible refresh to the user with potential state loss.

Because of that limitation, some developers/companies are tempted to use authorization code grant instead of implicit grant and also send the long living refresh token to the JavaScript client, giving up on security in favor of usability. The Azure Portal is one of those examples.

The refresh token found in the wild

Not as stateless as promised

The JWT promises us stateless authentication, but in practice you’ll almost always end up doing a lookup for every request. This will be to check if a token has not already been revoked, to verify additional authorization rules, etc.

Static resources

Static resources like images (<img />-tag) or downloadable content (<a />-tag) are hard to protect with tokens. One options is to include the access token as a query string parameter, causing the token to also end up in the server logs.

Advice of the OAuth2 IETF working group

At the end of 2018, the OAuth2 IETF working group advises against using OAuth2 (and thus also OpenID Connect) for SPAs served from the same domain as the API (which is probably also the case for your SPA).

For simple system architectures, such as when the JavaScript
 application is served from the same domain as the API (resource
 server) being accessed, it is likely a better decision to avoid using
 OAuth entirely, and just use session authentication to communicate
 with the API.
OAuth and OpenID Connect provide very little benefit in this
 deployment scenario, so it is recommended to reconsider whether you
 need OAuth or OpenID Connect at all in this case. Session
 authentication has the benefit of having fewer moving parts and fewer
 attack vectors. OAuth and OpenID Connect were created primarily for
 third-party or federated access to APIs, so may not be the best
 solution in a same-domain scenario.

Read the full draft here.

What’s the alternative?

The alternative for the aforementioned scenario is SameSite cookies. When the “SameSite” cookie attribute is set on a cookie, the browser will only send the cookie to the server when both the SPA and the API are on the same domain. Which is an effective protection mechanism against CSRF.

As you can see in the above image, browser support is fairly good. If, for some reason, you also want to protect users with older browsers, the good-old Anti-Forgery token (CSRF token) will do the trick. Luckily, most server-side frameworks make it easy to work with Anti-Forgery tokens as it’s a solution that already exists for decades.

Tokens still have their use

While we shouldn’t use tokens anymore for our SPA, tokens still have their use in other scenario’s:

  • JavaScript browser-based clients calling cross domain API’s (f.e. SPA on top of a micro-service architecture without API gateway)
  • Service-to-service calls
  • Single Sign-On scenario’s

Wrapping up

Apart from all the above, there are a few more thoughts I want to share with you regarding securing your SPA:

  • Stop using HTTP, even during development. Use HTTPS.
  • CORS Allow all? Of course NOT! Never allow everything in CORS, except you’re creating an extremely public API. During development, you might have to allow one or more addresses when running back-end and front-end separately. In production, since your SPA is on the same domain as the API: disable CORS.

There, that’s all I’ve wanted to say for now. I hope the article is of any use. Feel free to leave a comment if the article has helped you, you want to discuss something or you just want to point out a major mistake.

Have a nice day!