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 api.grabyo.com
.
Product constraints
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.
In-browser solutions
Given the AuthN and latency constraints, there are a couple of conventional ways we could achieve this in the browser:
Widely-scoped cookies
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.
This approach also faces limitations because, in a browser’s DOM, there is no way of attaching this token to any request except a Javascript 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 Authorization
header using Javascript because the browser does not expose the ability to intercept requests from HTML elements in the DOM 👻.
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 <video>
and <img>
.
Within this Worker, we can read the request’s URL and choose with Javascript whether or not to attach a JWT, so we also get granular control of the Authorization
header.
Security Considerations
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.
Caveats
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.
Non-HTTP protocols
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.
Browser support
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.
Non-standard
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.
Summary
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.
We’re hiring!
We’re looking for talented engineers in all areas to join our team and help us to build the future of broadcast and media production.