Sitecore leaning toward a composable architecture with a mostly headless development model, where all the business logic is shifted to a head app, which also becomes a major point of integration, the majority of which requires some sort of authentication.

TL;DR Do not work with an authentication token on a client! But what if I do have a need for it? Then, read the rest of this blog post to understand what your risks and options are.

In the article, we will look at the reasons for the need to work with the token on the web application client and find out what suits better for storing the token: localStorage, sessionStorage, or cookie without the HttpOnly flag (the answer: none of these!). We also take a look at the measures to be used for reducing the risk of token leaks through various vulnerabilities.

Introduction

The ways of doing authentication and authorization correctly are one of popular topics in web development. There are various good, bad, and evil practices I still encounter in everyday life while poking some websites with dev tools. With this article, we are going to talk them through. I want to pay attention to the problem, explain its significance, and also advise the approaches for improving security when working with tokens on the client.

The problem

Faulty authentication implementation while working with tokens on a client increases vulnerability. The common scenario: having an access token for some resources exposed on the client side, followed by XSS attack gives thefts the same access to the same data as the legitimate scripts would have.

One would say:

…we’re working with %framework% or %library% where all issues should be accounted for. Why would I ever bother at all?

Sounds reasonable, doesn’t it? However, the reality is not that straightforward, and in most cases, it is almost impossible to guarantee safety. There might be situations where security threats originate from completely different places. Imagine, placing the token into a wildcard non-HttpOnly-cookie and securing the application from XSS to the absolute, still does not prevent a breach, just because one of the subdomains holds a vulnerable project which neglects all these safety measures.

But why is a stolen access token dangerous? With it, attackers will be able to make requests to API resources on our behalf or simply impersonate our account in their browser, getting the same level of access.

Evil Case #1: Access token in localStorage

The application receives an access token from the backend and saves it into the localStorage. In this case, if there is an XSS vulnerability, the token can be obtained by an attacker. Let’s say, the token has a lifetime of 15 minutes. Is there a risk of attackers using the token within less than 15 minutes? Absolutely! In my opinion, the risk is too big for this approach to be considered.

Evil Case #2: A pair of access and refresh tokens in localStorage

An advanced case of the previous example. An access token has the same lifetime, but there’s also a long-live refresh token – a string used to get a new refreshed access token. While an access token is used to access a protected resource, a refresh token allows you to apply for a new (or additional) access token. As in the above example, an XSS attacker can gain access to both an access token as well as a very long-lived refresh token.

There is a set of measures to improve the security of refresh tokens:  token rotation, protection against re-use, proper (reasonably balanced) selection of lifetime, and others. However, these measures should complement and add on rather than replace XSS token leakage protection.

Evil Case #3: Access token within a wildcard non-HttpOnly cookie

I have come across this approach several times and it carries the greatest risks of all those above. I believe, the intention of using a wildcard cookie is supposed to be for “seamless” authentication between subdomains, but I would like to warn against the careless usage of such an approach.

The catch is that the token placed into a cookie becomes available for all the subdomains of a given site. Even if an attacker fails to find an XSS hole in the main application, a vulnerable service might exist among the numerous subdomains. Actually, scanning subdomains is a typical technique conducted in pen testing. subdomains may contain a lot of filthy things: a legacy site, test project, sandbox/playground, unprotected APIs, or any similar potentially vulnerable service. In addition to that – there might be also a subdomain takeover attack which also empowers an attacker to steal the token in a similar way.

Why at all then one needs to work with a token on a client?

The logical question coming from the above is: why at all do we need to make the token available on a client and work with it there? Why not just put it in a session-hardened cookie? Other than poor application design, I could name a few valid reasons.

1. Using stateless tokens

Unlike “stateful” tokens that serve as a key to session-based data kept at a server, this token is self-sufficient and contains all the required information for authorization. Typically, JWT tokens are used for that purpose, and they have a standard structure:

Due to the JWT signature block, the validity could be verified upon a receipt of request having such a token (either a private key for symmetric encryption or a public key for asymmetric encryption). That means JWT validation takes place without the issuing server (ie. no need to reach a database for each validation).

The approach allows is suitable to stay away from storing tokens at a server by issuing such a JWT token upon the authentication and keeping it at a client.

2. Using OAuth 2.0/OIDC for authentication in your own application

A fairly popular thing is to use, for example, the Authorization code grant flow in OAuth2.0 or OpenID Connect (OIDC).

Some confuse OpenID Connect with OAuth 2.0 –  in fact, OIDC extends OAuth 2.0 and is an authentication protocol, while OAuth 2.0 is natively an authorization protocol. However, you can often see implementations of OIDC on top of OAuth 2.0. The difference is that in the case of the OIDC Authorization Server, the Resource server also plays a role, but only for the user’s identity.

3. Using SPA without a backend

Without relying upon a backend, developers are forced to look for ways to work with the token on the client. Disputes on the Internet used to be popular where it is better to store a token to work with it: in localStorage, sessionStorage or cookies (of course, without HttpOnly). In fact, in terms of security, there is practically no difference. And that’s why:

localStorage

sessionStorage

Cookies without HttpOnly flag

Available from a client

                     Yes

                                    Yes

                        Yes

Domain binding

Yes

Yes

Yes, even wider than  wildcard cases

Context

Synchronizes between tabs

Limited within a tab

Synchronizes between tabs

       Persistency

Persists upon closing a browser

Persists upon refreshing a tab but voids upon tab closure

Persists upon closing a browser

What about IndexedDB?

IndexedDB is no different from localStorage technologically, in terms of access it is very similar, with one difference: service workers also have access to it.

Considerations

As it comes from above, all the above methods are equally vulnerable to malicious code at a client. Whatever is available from the developer’s code is available to the attacker’s code.

But what should we do then? There is no silver bullet, various approaches carry on their pros and cons, are bespoke projects-specific, and must be chosen at a design phase.

Approach #1. Not to work with the token on the client at all

The first and the most straightforward way of avoiding risks is refusing to work with the tokens on the client. A stateful approach to authentication could be preferred instead of stateless. In that case, a session cookie would store the token. It is crucial to ensure a cookie will require the correct setting of the HttpOnly, Secure, SameSite, and Path attributes. In addition, I discourage you from mindlessly using wildcard cookies (for the reasons discussed above), therefore you should also pay attention to the Domain attribute.

One thing to keep in mind: since this cookie will be sent to the server within requests to the specified Domain and Path, it is necessary to provide protection against CSRF attacks (as per OWASP recommendations).

This approach takes away the benefits of stateless tokens but makes risks transparent and is easier to implement.

Approach #2. Using proxying backend

But what if we must retain the ability to work with stateless tokens? In that case, you should use a middleware layer that will ensure the security of storing the token.

This can be visualized in a diagram. First, see how a simplified process of authentication and reaching a resource looks without a proxying backend:

The above diagram carries on all the previous issues. Let’s add an intermediate backend proxy:

In this diagram, the backend proxy is acting not as a public, but as a confidential client (RFC 6749). It must request a token with its client id and client secret.

While authenticating and receiving a token, we’re not calling the authorization server directly, instead, we’re relying on Backend proxy [1]. Therefore Backend proxy is the one that receives the access token [4]. Then server generates a cookie with HttpOnly flag to be sent to a client [6].

Next, the client calls the API resource, however not directly, as it does not have a token, but through a proxy [7]. Authentication takes place there as well as the exchange of a cookie to a valid access_token [8] to be used for requestion Resource server [9] so that the received response gets proxying back to the client [12].

The question comes: which cookie can Backen proxy create and return?

An ordinary session cookie counterpart to the received token. In this case, we will have to implement work with sessions and store them. However, this empowers us to manage access invalidation.
Encrypting the values received from the Authorization server. In this case, we avoid the need to store the session. Upon a request to a resource, we decrypt the cookie value (as we got the key for this) and proxy the request further.

You have to care about the cookie expiration date and also be responsible for setting other attributes. Separately, you should care about selecting the lifetime for an access_token, caching it at Backend proxy, updating an access_token, etc.

This approach could be also applied to SPA, as swith SPA it is not mandatory to entirely give up the backend. In a microservice architecture, the Backend proxy could be done by API gateway or Backend-for-frontend (BFF).

So, this approach gives us the opportunity to protect against token theft through XSS, however, it is vulnerable to CSRF attacks, so you must protect against it.

Approach #3: Use a service worker

Service worker – a script that runs in the background in a browser that acts as a proxy server for comms between the application, browser, and the network. The service worker runs in a separate context, runs in a separate thread, does not have access to the DOM, and accordingly, the client also does not have access to the service worker and the data there. We will use this feature of it to protect the access token from leakage during XSS.

In this case, the service worker is responsible for getting the token from the Authorization server and making requests to the Request server. In this case, requests from the client are proxied by the service worker, and it seems to intercept them. Therefore, the call to the getter method and the token itself are completely isolated, since the service worker context is not accessible to other JavaScript contexts.

Reminds of the previously discussed scheme with a proxying backend, isn’t it? The difference is that the middle layer, acting as a proxy is safely implemented directly on a client, not on the server.

In this case, CSRF attack is not possible, as the token is only available to the service worker, the possibility of stealing the token through XSS is also non-existent. It comes at a cost of more complicated implementation. And of course, service worker is supported by all modern browsers.

References

OAuth 2.0 Security Best Current Practice
OAuth 2.0 for Browser-Based Apps
OWASP JSON Web Token Cheat Sheet for Java
auth0.com – Token Storage
Authentication in SPA (ReactJS and VueJS) the right way
Getting Token Authentication Right in a Stateless Single Page Application
Best OAuth Security Practices for Single Page Applications