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.
_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.
In addition to this, there is also a risk of inadvertent mistakes from developers leaking access tokens.
Using Web Workers to store access tokens
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.
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
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.
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.