There's a couple of ways you can authenticate your API calls when calling them from a service worker, and a lot of it depends on how your authentication system works. For cookie based authentication, i.e your service sets a HTTP only cookie to identify a request, you don't need to do anything fancy as the fetch
call will automatically include the cookie along with the request.
If your service happens to use JWT tokens or some other form of token where you have to add the token or code into your request's header, it becomes a little tricky. In the frontend, you would have created a fetch
wrapper that adds the token to every request by either getting the token from a store (like Redux/React Context) or by getting it from the local storage. Both of the above examples will not work in the service worker as it cannot access both Local Storage and your store.
PS - See solution #2, my personal favorite.
Getting the token from frontend and storing it in-memory
The most popular way would be use the postMessage
API to communicate between frontend and SW. Once the SW is installed and activated, we can attach a listener on the client side (frontend) to listen to any messages from the SW and respond back.
// I like create a separate React component with the sole purpose of handing SWs
export const ServiceWorker: FC = () => {
useEffect(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js', { scope: '/launcher' })
.then((registration) => {
registration.action.postMessage({
action: 'AUTH_TOKEN',
payload: authToken // your auth token from a store.
})
})
.catch(() => {})
}
}, [])
return null
}
This particular piece of code registers the service worker, waits for it to be registered and then immediately posts a message with an action and payload. Side note - it is always a good practice to use the action pattern to specify what kind of data is being passed. Comes in handy when trying to make sense of things.
In the service worker side, we need a way to handle the message and store the token.
self.addEventListener('message', (event) => {
if (event?.data?.action === 'AUTH_TOKEN') {
setAuthToken(event.data.payload)
}
})
The above is a simple event handler that listens for a message
and handles the AUTH_TOKEN
action accordingly. The setAuthToken
is just a simple function that stores the token in a var
. The variable lives in a separate file and can be imported wherever necessary and it will be in-memory till the lifetime of the service worker.
Disadvantages of doing this
- Over the years I've come to realize that message passing is not that reliable.
- While storing it in-memory, if the browser decides to garbage collect the service worker, you will lose the token.
- Logging out the user from the client would need you to post another message to wipe the token from the service worker, because even if the user is logged out, SW would still be installed and continue to execute if necessary.
- We have to wait for SW to be installed, then send the token. Browsers are very weird when it comes to installing and activating updates to service workers (even if you do
skipWaiting
).
Solution #2 - using IndexedDB
Life would be a lot simpler if server workers were able to access Local Storage, but they cannot. What they can do is access IndexedDB, which can also be accessed by the frontend. The ideal solution to sharing auth token between frontend and service worker would be to write the token to a store when authentication happens and simply remove it when we log the user out. This provides a lot of advantages over message passing:
- Reliable storage and access of token - IndexedDB is well tested and a very good way of persisting data.
- Control of the token remains with the client - can be removed or added whenever necessary.
- No need for message passing between client and SW - write in the client side, read-only in SW.
You simply store the token and don't have to worry about the service worker being activated, or if it's pushed out of memory.
A very simple snippet below gives an example of how to read and write to IndexedDB.
// Writing to DB. On the frontend when user authenticates.
const dbConnection = indexedDB.open('db', 1)
dbConnection.onsuccess = () => {
const transaction = dbConnection.result.transaction('authToken', 'readwrite')
const store = transaction.objectStore('authToken')
store.put(authToken, 'AUTH_TOKEN')
}
// Reading from DB. On the service worker.
const dbConnection = indexedDB.open('db', 1)
dbConnection.onsuccess = () => {
const transaction = dbConnection.result.transaction('authToken', 'readonly')
const store = transaction.objectStore('authToken')
const data = store.get('AUTH_TOKEN')
data.onsuccess = () => {
console.log(data.result) // Your auth token
}
}
Reliable. Simple. Easy to maintain.