Leveraging web workers to safely store access tokens

We use web workers to offload resource-intensive tasks to a background thread in a web app. But did you know we can also use them to safely store access tokens? This article discusses how it can be done and the advantages of doing so.

Access tokens are widely used to authorize users to access restricted resources such as an API endpoint. However, in Single Page Applications, storing access tokens safely becomes a huge challenge.

The usual practices

The usual practice is to store access tokens in the session storage or the local storage. But these methods are susceptible to Cross-Site Scripting attacks. Besides, malicious third-party libraries can easily access these tokens.

Most guidelines, while advising against storing access tokens in the session or local storage, recommend the use of session cookies. However, we can use session cookies only with the domain that set the cookie. If the server that authenticates you belongs to one domain and you want to access resources in a different domain, then session cookies will be moot.

JavaScript closures

Another popular suggestion given is to store access tokens in the browser’s memory. This is a safer method than storing access tokens in the browser storage and more convenient than using session cookies. We use JavaScript closure to make sure that third-party libraries and Cross-Site Scripting attacks cannot compromise the token.

JavaScript closures are a way to implement private attributes you find in most of the Object-oriented programming languages. The following is an example of a JavaScript closure.

Here, the _privateVariable cannot be accessed from outside the function. By using this method, we can safely store access tokens in memory. However, there is a caveat. Even though third-party libraries cannot directly access the access token, they can intercept an http request and extract the token from the header.

We usually send access tokens as a header attribute of API requests to access protected resources. You can use JavaScript to eavesdrop on such calls and obtain the header fields. The following code demonstrates how we can accomplish such a thing.

In addition to this, there is also a risk of inadvertent mistakes from developers leaking access tokens.

Using Web Workers to store access tokens

We can address most of these issues by simply storing access tokens in web workers. Now, what are web workers? Web workers are essentially background threads. We all know that JavaScript is not a multi-threaded language. Having to execute everything on one thread, at times, resulted in slower user interfaces. This affected the user experience. To rectify this, web workers were introduced.

Generally, developers offload resource-intensive tasks to web workers. This frees the main thread to concentrate solely on the user interface allaying user experience issues. So, how exactly can web workers solve the problems associated with storing access tokens?

To begin with, web workers run in a context that is different from that of the main thread. This means that the code running on the main thread cannot access the code running on the web worker and vice versa. The only way a web worker and the main thread can communicate is by exchanging messages.

It is the developer who is responsible for designing how the main thread and the web workers communicate. They can communicate through a simple request-response model. So, by ensuring at no point the web worker sends the access token as a response, we can prevent third-party libraries and Cross-Site Scripting attacks from getting hold of the access token.

Why not use a Service Worker or an iFrame instead of Web Workers?

It is pertinent to ask why we cannot use a service worker or an iframe instead of a web worker. Just like web workers, both service workers and iframes have different browsing contexts. However, unlike web workers, service workers and iframes have a global reference. This means that codes can access them from anywhere.

In contrast, we can keep a reference to a web worker private using JavaScript closure. This would mean that unless you have access to the private variable that refers to the web worker, you won’t be able to refer to the web worker. This adds an extra layer of protection by preventing third-party code from even trying to communicate with web workers.

Furthermore, a site shares service workers over different tabs. This means, only one user can be logged in at a given time. Contrary to this, a web worker is specific to a tab and this allows multiple users to be logged in concurrently.

In addition to preventing third-party code from accessing access tokens, web workers also prevent http-request interceptions. Since third-party codes run on the main thread, they can’t intercept requests initiated by the web workers. Yes, when we store access tokens in web workers, API requests needing those access tokens should also be initiated from the web workers.

The architecture of the solution

Inserting image...
The architecture

Now, let’s discuss how the architecture of this storage mechanism would look like. To make sure the web worker receives the access token, it is the web worker that should make the request for the token. However, the request for sign-in will have to come from the main thread.

So, once a user hits the login page of the app, a post message (postMessage) should be dispatched to the web worker. On receiving this message, the web worker can then initiate the authentication flow. Once we receive the token, we can store it safely in the web worker.

When API requests are to be sent, once again a message should be sent from the main thread with the necessary details to the web worker. Then the web worker can initiate a request with the access token attached to the header. Once we receive a response, we will have to communicate it back to the main thread as another message.

An easier way to understand this is to imagine a web worker as a separate gateway server. Any communication between the Single Page Application and a protected backend will be taking place through this gateway.

However, this creates a new front for attacks. An attacker can initiate a request to a URL under their control. Since the web worker attaches the access token to the requests, the attacker will be able to receive the access token at their end.

We can prevent this by passing the allowed URLs to a web worker during the initialization phase. The web worker can then check if the URL to send the request to matches one of the allowed URLs before dispatching the request.

The flipside of using Web Workers

Even though this method of storing access tokens has its many advantages, it is not without its shortcomings. The major one of these is that every time we reload the page, we will lose the login session. This happens because, with web workers, we store the access token in the browser memory. Every time we reload a page, the browser resets the memory. This would mean the users may have to go through the login flow once again.

Most OAuth2/OIDC providers have ways of addressing this issue. For instance, some use session cookies to identify logged-in users. So, these providers will straight away return the access token without having the user enter their login credentials. Thus, despite having to go through the authentication flow on page reloads, the user experience does not change.

The need to use third-party libraries in the web worker poses another challenge. For example, to send API requests from the web worker, you may have to use certain libraries. These libraries will be running in the context of web workers. Hence, these libraries may get access to the stored token. However, by limiting the third-party libraries used in the web worker to a bare minimum and using only trusted ones, we can largely mitigate these issues.

Nonetheless, this method is a lot safer than storing the access token in the main thread as the number of libraries we use in the main thread is usually higher than what we use in the web worker.

Summary

Storing access tokens in the browser is a challenge. When comparing all the available methods, web workers seem to provide the safest and most convenient option. Using web workers, we can prevent Cross-Site Scripting attacks and malicious third-party libraries from stealing access tokens. In addition, we can also prevent malicious codes from intercepting network requests and thereby gaining access to the tokens. However, one needs to be careful about the libraries that they use in the web worker and should try to keep it to a trusted few. And the cherry on top of all these things is that by using web workers to send and receive network requests, we are actually reducing the load on the main thread.

1 Comment

Leave a Reply

placeholder="comment">