Clurgo logo
  • Clurgo
  • Blog
  • Service-worker and static content authorization

Service-worker and static content authorization

6/25/2021 Krzysztof Boczkowski

Share

Many web applications display various content in the browser. Many of these are only available to logged users or those with a specific role. Often API is secured by JSESSIONID or JWT cookie. While in the first case there is no problem with using the browser, in the case of JWT there is one problem. What is it?

In addition to ordinary data in the form of JSON, where data is transferred in the form of text, we also have other resources that we want to protect. An example are graphics / photos to which we do not want to give unauthorized access. What is the way to do it? We can forward them to the front in BASE64 format, but this unfortunately increases the file size. This is undesirable in web applications. We can also pass them in binary form, where content-type is set to image / *. Here the size does not change. But how to display such a resource? The easiest way to do this is to use the HTML tag. In a regular query, e.g. using the Fetch API, we pass the Authorization header, in which we add our JWT token. Unfortunately, we do not have such a possibility in the case of this a tag. But are you sure? We can use the service-worker for this.

Fig 1 – Example secured resouce

A bit of an introduction…

Before I describe how to do this, let’s briefly see what exactly service-worker is. This tool is used as a proxy in the client-server architecture, where it is possible to cache data when there is no network connection. This is extremely useful because it allows you to simulate a native application in a browser window, e.g. by saving the application as a PWA on a mobile device.

The service worker itself is a script run in the browser that works independently of the page visible on the screen. From the code level, we cannot access the DOM tree or the Web Storage API (localStorage, sessionStorage). The only way to pass data to such a worker is API Client.postMessage () or IndexedDB database available in all browsers.

Ok, but how to use it?

At the very beginning, let’s try to register such a script in the browser. The first step is to add the script initialization to the code of our application. The code snippet below checks if the browser supports this solution and points to the file with the code of such a worker:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then((registration) => {
        console.info(
          'ServiceWorker registration successful with scope: ',
          registration.scope,
        );
        return registration;
      })
      .catch((error) => {
        console.info('ServiceWorker registration failed: ', error);
      });
  });
}

Then we need to create such a file in the folder that will be hosted on the server. Usually in projects there is a public folder that contains transpiled and minimized production code and static content. Let’s create a file called service-worker.js. Let’s add the code to it:

self.addEventListener('install', function (event) {
  // Skip the 'waiting' lifecycle phase, to go directly from 'installed' to 'activated', even if
  // there are still previous incarnations of this service worker registration active.
  console.log('install');
  event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate', async function (event) {
  console.log('activate');
  // Claim any clients immediately, so that the page will be under SW control without reloading.
  event.waitUntil(self.clients.claim());
});

Now let’s check if our service worker has been registered correctly in the application. Let’s run it locally and in the browser’s developer tools console we should see the following calls:

Fig. 2 – Correct registration of a service-worker

Fig. 3 – Preview of the status of the service-worker registered in the browser

Ok, but how do I add the authentication header now?

We already have a properly registered service-worker, but what next? How to authenticate the HTTP request for such a resource? If we already have in our application JWT support for downloading content, there is nothing else to do but to use such a token in a place where we have its implementation and pass it to the service-worker.

As I mentioned earlier, this script unfortunately does not have access to the Web Storage API, so let’s use the IndexedDB database. For this purpose, let’s add a dependency to our application:

$ npm i --save idb

IDB is a library based on Promise object, and this will make it easier to work with this database that uses callbacks mechanism to operate on transactions. So let’s try to create this database and store our token in it:

const dbPromise = idb.openDB('appDB', 1, {
      upgrade(upgradeDb) {
        if (!upgradeDb.objectStoreNames.contains('token')) {
          upgradeDb.createObjectStore('token', { keyPath: 'key' });
        }
      },
    });

    dbPromise.then((db) => {
      let tx = db.transaction('token', 'readwrite').objectStore('token');
      tx.put({ key: 'token', value: msg.data.token });
    });

Ok, we already have our token saved. But now we have to read it from the service-worker and add the appropriate header to the query. To do this, we need to connect to the event fetch in the service-worker.js file, which is used to download all resources via HTTP. In such a listener, we need to connect to specific URLs to intercept the query and modify it. Read our token from the database and use it for your request. So let’s try to write this support:

self.addEventListener('fetch', function (event) {
  event.respondWith(
    (async function () {
      let response = undefined;
      if (event.request.url.includes('/resource')) {
        if (!('indexedDB' in self)) {
          console.log("SW.js: This browser doesn't support IndexedDB");
          return fetch(event.request);
        }

        const dbPromise = idb.openDB('newExpertise', 1, (upgradeDb) => {
          if (!upgradeDb.objectStoreNames.contains('token')) {
            upgradeDb.createObjectStore('token', { keyPath: 'key' });
          }
        });

        const token = await dbPromise.then((db) => {
          let tx = db.transaction('token', 'readonly');
          let store = tx.objectStore('token');
          return store.get('token');
        });

        response = await fetch(event.request, {
          mode: 'cors',
          credentials: 'include',
          headers: { Authorization: `Bearer ${token.value}` },
        });
      } else {
        response = await fetch(event.request);
      }
      return response;
    })(),
  );
});

That’s all?

Yes, now we can check whether an authentication token is added in requests for photos and whether we will get any data at all. In the browser’s development tools, in the Network tab, we should be able to see our query with its headers and status:

Fig. 4 - HTTP request response

Fig. 4 – HTTP request response

We managed to easily and quickly handle the download and display of photos hidden behind the security of the system. This is a simple example that can be adequately expanded by, among others, caching this content using the Cache API. This solution has an advantage over other methods, because it allows for the previously mentioned caching of resources, but also simplicity and universality of operation. We have an implementation in one place that will not be a problem for developers. They don’t need to remember to use some tool or specific implementation to download such a resource. It works regardless of the application and you don’t have to worry about it 😉

Clurgo logo

Subscribe to our newsletter

© 2014 - 2024 All rights reserved.