Leveraging Web Workers to Safely Store Access Tokens

WSO2 sponsored this post.
Web workers are used to offload resource-intensive tasks to a background thread in a web app. However, did you know we can also use them to safely store access tokens? This post explains 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 significant challenge.
Common Practices

The usual practice is to store access tokens in the browser’s session storage or local storage. This is because we need to persist access tokens across page reloads, to prevent the need to re-authenticate on every reload. This provides a better user experience. However, these methods are susceptible to cross-site scripting attacks and 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 sets the cookie.
Another popular suggestion 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. JavaScript closure is used to make sure that the token stored in the memory cannot be accessed by third-party libraries and cross-site scripting attacks.
However, there is a caveat. Even though the access token cannot be directly accessed by third-party libraries, they can intercept an HTTP request and extract the token from the header. There is also a risk of developers making inadvertent mistakes by 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. They 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 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 that 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 iframe?
Both service workers and iframes offer different browsing contexts. 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 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 cannot 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
Now, let’s discuss what 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. Upon receiving this message, the web worker can then initiate the authentication flow. Once we receive the token, it can be stored 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.
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 matches one of the allowed URLs before dispatching the request.
The Flipside of Using Web Workers
One of the problems with web workers 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 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.
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 mitigate cross-site scripting attacks and malicious third-party libraries stealing access tokens.
Additionally, we can also prevent malicious codes from intercepting network requests and gaining access to the tokens. However, you need to be careful about the libraries you use in the web worker and you should try to keep it to a trusted few. By using web workers to send and receive network requests, we are actually reducing the load on the main thread.
WSO2 sponsored this post.
Feature image via Pixabay.