DEV Community

Cover image for Re-architecting authentication with Service Workers
Gaurav Behere
Gaurav Behere

Posted on

Re-architecting authentication with Service Workers

A use case of changing the authentication mechanism of a web application without touching a lot of legacy codebase



Many times you would encounter situations where you have a legacy codebase in front of you which has been in the market for quite a while. It may be written in a technology that is seeing a downward trend in terms of popularity. You cannot make a change very easily in the architecture of such applications as the amount of risk, testing efforts & impact is huge.
Let me run you through such a use case where we recently had to change the authentication mechanism of an existing legacy web application from a JSP session & a cookie-based authentication mechanism to an MSAL (Microsoft Authentication Library) token-based authentication method.
What this essentially means is the login should grant a token to the web application acquiring the token using MSAL (react-msal) in our case & the same token should be used for making further calls to the server.
Read more about MSAL tokens here:
https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/acquire-token.md

Challenges

There are two challenges we are looking at:

  • Changes to the webserver: The web server should be able to authenticate our requests with the token that the client application is going to send as a bearer token.
  • Changes to the legacy UI code written in JSP: The amount of legacy code which is an amalgamation of many UI technologies where there are requests like POST from FORM submits, XHR calls, calls through native JS fetch, Jquery’s $.ajax & a bit of axios too. It becomes very hard to avoid changes in almost every part of the code & still get the new authentication mechanism working where every call to our server should have a bearer token attached to the HTTP header.

Adding to the complexity of the application, the code base grew up with a lot of acquisitions of companies adding to the integrations in the existing codebase. Thus the application grew horizontally in terms of technology over the last 10 years.

Also when you have such a legacy codebase, it becomes hard to keep the knowledge fresh. There are pieces of the code that developers might not have even looked at for a long long time. Touching such code may result in unaccounted for side effects as the application has a significant number of customers who are using different versions & flows of the application.


How can we have a centralized solution which avoids making changes to a lot of files?

Service workers & promises to the rescue.
We try to avoid changes in the front-end code updating the APIs to authenticate based on the incoming MSAL token.
The solution is to capture all the network calls originated from the web application & append a bearer token in the HTTP header in the request.

  • Take a hold of all the network calls generated by your web application using a service worker registered at the root of your web application.
self.addEventListener('fetch', (event) => {
  const token = "some dummy token"; // This needs to be requested from MSAL library

  // Responding with a custom promise
  const promise = new Promise((resolve, reject) => {
    // edit event.request & respond with a fetch of a new request with new headers
    let sourceHeaders = {};
    for (var pair of event.request.headers.entries()) {
      sourceHeaders[pair[0]] = pair[1];
    }
    const newHeaders = { ...sourceHeaders, 'Authorization': 'Bearer: '+ token };
    const newRequest = new Request(event.request, {headers: newHeaders}, { mode: 'cors' });
    resolve fetch(event.request);
  });

  event.respondWith(promise);
});
Enter fullscreen mode Exit fullscreen mode
  • In the fetch event we need to respond with a new request that has HTTP headers we need. In the gist above, we are just adding a dummy auth token to the request. Here we do a couple of things:
a. We copy all the headers of the incoming request.
b. We create a new request with incoming headers & a new authorization header containing a token.
Enter fullscreen mode Exit fullscreen mode

Now let us get the right token.

Here comes the tricky part. A service worker comes with its own limitations, it has no access to DOM & it can’t access shared storage between the page & itself. Somehow we need to get the token from the main thread & the container app.
Here is a good article explaining how to establish communication between a service worker & the container page.

https://felixgerschau.com/how-to-communicate-with-service-workers/

We choose the Broadcast API to get away with the need of the two parties to remember the ports to have a 1:1 communication channel.


// Create a channel for communication
const channel = new BroadcastChannel('TOKEN_EXCHANGE');

const getAToken = () => {
  const promise = new Promise((resolve, reject) => {
    // Listen to token response
    channel.onmessage = (e) => {
      resolve(e.data);
    };
    // Send a token request to the main thread
    channel.postMessage("TOKEN_REQUEST");
  });
  return promise;
}
Enter fullscreen mode Exit fullscreen mode

Changes in the container app

The container app now needs to listen to the messages on the same broadcast channel & respond with a token.
This allows up to keep the front end legacy code as-is & at the same time have a new authentication mechanism.

Things to note

  • As our solution is based on service workers, promises & Broadcast API, browser compatibility can be a limitation.
  • We still had to re-factor the APIs to honor tokens in the request for authentication.

Discussion (0)