Service workers to manage our JWT tokens
Managing authentication (AuthN) on the web can be challenging, especially in a micro-service architecture. Web security features such as Cross-Origin Resource Sharing (CORS) and cookie attributes aim to improve HTTP mechanism security. Still, these can lack the controls and granularity needed to implement custom solutions securely.
Many of our micro-services are accessed via separate subdomains but authenticate requests using the same token. For example, a user may be logged in on
store.grabyo.com but needs some data from
api.grabyo.com. Using the same JSON Web Token (JWT), which controls their session on
store.grabyo.com, they can authenticate their request to
The one seemingly simple solution would be to put our services behind a single “gateway” subdomain, but specific product requirements prevented us from doing this. So instead, this solution would typically involve setting up a proxy server that would handle all incoming requests and forward them to the relevant services.
Grabyo’s business is focused on live video, so low latency is essential, but we serve users worldwide, which makes managing latency a challenge. For example, if a request has to go through a Gateway service before reaching its destination, that adds latency, so we wanted to avoid that. “Edge” computing solutions aim to alleviate this issue, but this still introduces another hop and thus latency for the request.
On top of that, some of our video technologies require different technology stacks and subdomains, making using a single “gateway” impossible.
The optimal solution would send the request directly from the user’s device to the end service, avoiding as many proxies, gateways and DNS lookups as possible.
Given the AuthN and latency constraints, there are a couple of conventional ways we could achieve this in the browser:
We could set the JWT in a cookie scoped to
*.grabyo.com. This would result in all requests to any
grabyo.com subdomain being sent off with a JWT in the cookie header to work. However, not all of these services implement functionality that requires that authentication token.
According to the Principle of Least Privilege, a service should only have access to information it needs, so we didn’t want services that did not require the JWT to be sent, which increases the risk of leakage.
“Authorization” HTTP Request Header
Attaching the JWT to an XHR request’s
Authorization header would allow us in the application code to choose whether or not to attach the JWT to any given request.
XMLHTTPRequest. In our product, we have a requirement to use specific subdomains for certain media delivery features, which are often included using HTML elements i.e.
<video>, <audio> or <img>. In this scenario, we cannot attach the
We needed a solution in the browser which allowed us to ensure:
- JWTs are sent with requests only when needed
- We can choose to send JWTs with cross-origin requests initiated by HTML elements
- The request is routed from the user to the service as directly as possible
Enter Service Workers
A Service Worker is a type of Web Worker that acts as a programmable network proxy for your website and lives in the user’s browser between the Document and the browser’s underlying network interface.
Service Workers are most commonly used to provide offline capabilities to sites, a vital part of the PWA standard. This is achieved by using Service Workers to cache responses in the browser and serve those cached responses when the browser can’t reach the internet.
This proxy behaviour can also be used to intercept and modify the headers of any HTTP requests initiated by the Document, including those from HTML tags such as
The Worker has to have access to the JWT, so it’s essential that a hacker cannot steal it via the Worker.
Service Workers are installed in the user’s browser, but the installation is limited in scope, so they only activate on the origin and path on which they are registered. External sites have no means of accessing the Worker’s state.
A Worker runs in a separate thread to the main thread, meaning that the Document has no access to its state or memory. The Document can only communicate with a Worker via the postMessage API. By controlling what data the Worker sends to the Document, you can prevent a Cross-Site Scripting (XSS) attack from stealing the token.
Versioning and deployments
As a Worker is installed on a user’s device instead of being retrieved from your server on every request, delivering updates to the Worker code is different from a conventional web page. Therefore, you need to take care when shipping updates to the Worker, ensuring each version is backwards-compatible.
Service Workers cannot intercept WebSocket initiation, so we had to implement a different AuthN mechanism for those. However, cookies can be sent with WS connections, so this is one area where cookies have an advantage.
Some browsers such as Firefox have a setting that allows users to disable Service Workers. Without a fallback mechanism, the application won’t work for those users. Maintaining two security implementations increases risk, so we have to decide how to handle a scenario where Workers are unavailable.
This is a slightly unusual way of solving this issue and an unusual way of using Service Workers. However, using tried and tested methods are generally a safer security approach, as not every problem will be readily apparent. In contrast, potential issues will have been identified and accounted for in a more widely adopted paradigm.
Service Workers are a powerful tool with a wide variety of applications, but they should be used with care. Also, while this helps to address some of our requirements, we still require alternative implementations for specific use cases.
If not for the requirements around reducing latency, we would probably opt for the single Gateway approach. Services like AWS’ API Gateway, Lambda@Edge and paradigms like Graph APIs make this a compelling and flexible approach. However, these can be pretty expensive, especially at scale, unlike Service Workers, which run in the user’s browser. Furthermore, the cost of building and maintaining global edge infrastructure is high, compared to a Service Worker built by one developer in a day or two.
Interested in joining our engineering team? We’re always on the lookout for talented individuals – check out our open roles here.