Re-architecting authentication with Service Workers
Tags: javascript, webdev, architecture, authentication
Published: 2021-09-13 06:06:42

A use case of changing the authentication mechanism of a web application without touching a lot of legacy codebase
<hr>
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
<hr>
## Challenges
There are two challenges we are looking at:
- <b>Changes to the webserver</b>: 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.
- <b>Changes to the legacy UI code written in JSP</b>: 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.

<img src="https://miro.medium.com/max/1050/1*kFSS6PQtTtxIG66AVRJOwQ.png"/>

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.

<img src="https://miro.medium.com/max/1050/0*6knY-qVrAJOBwvx1.png"/>

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.

<hr>
### How can we have a centralized solution which avoids making changes to a lot of files?
<b>Service workers & promises to the rescue.</b>
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.
<img src="https://miro.medium.com/max/1400/1*Qpmgo07lva2ft-0iMFMusA.png"/>


- 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.

```javascript
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);
});
```

- 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.
```

<b>Now let us get the right token.</b>

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.

<img src="https://miro.medium.com/max/1050/1*NFN2uS3PocIlm9Y0R6Xvrg.png"/>

```javascript
// 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;
}
```
### 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.