Dashboard 2.0 PWA: Service Worker prevents authentication redirect when session expires

I'm running Node-RED with Dashboard 2.0 behind Cloudflare Access (Zero Trust), which handles authentication. The dashboard is accessed via a public URL that tunnels back to my home network. I've installed the dashboard as a PWA on my iPhone (Add to Home Screen), so it opens in its own standalone window.

The Cloudflare Access session expires after a set amount of time. When this happens and I open the PWA, I expect to be redirected to the Cloudflare login page so I can re-authenticate. Instead, I see "There was an error loading the Dashboard" with the disconnected image and a "Reload App" button. Reloading does not help, it shows the same error every time.
The only way I've found to recover is to delete the PWA from my home screen and re-add it.
What's happening under the hood:
I captured HAR files from the failing sessions and the root cause seems to be that the Service Worker that Dashboard 2.0 registers caches everything, including index.html and the main JS bundle. When I open the PWA:

  1. The Service Worker intercepts the navigation request and serves index.html from its cache (status 200, _fetchType: "Service Worker")
  2. The cached JavaScript boots the Vue app and calls fetch("_setup")
  3. The _setup request goes to the network, but Cloudflare Access blocks it (session expired) and the cross-origin redirect causes the fetch to fail
  4. The catch handler shows "There was an error loading the Dashboard"
  5. No request ever reaches the network, so Cloudflare never gets a chance to redirect to its login page

Has anyone else run into this? Is there a configuration option I'm missing, or would this need a change in how the dashboard's Service Worker is configured?

1 Like

Hi,

We've seen similar issues with the service worker and I have spent hours trying to figure out how to fix it. In the end, the only reliable fix I could find was to disable the service worker entirely (which you can do in the dashboard base config node).

The problem comes down to the service working making http requests to revalidate its manifest - and those requests failing auth. I could not find anyway to programmatically react to those failing requests.This must be a common problem with service workers, but I couldn't find any useful tips on how to resolve it.

1 Like

On Android, refreshing the page sends it to the login page.

Oh, sorry, I see that I have disabled Install as App.

Something must receive the 301 redirect - is that in Node-RED or in the web worker?

Surely there must be a way to detect getting a 301 instead of a valid response?

That is what I assume, and what I have spent a lot of time trying to figure out. It's buried in the web worker - and the various frameworks layered on top. I couldn't find a straight answer for it.

.... and don't call me Shirely ....

Ah, is the web worker something that has been generated? Sorry, I've not looked at the WW in D2.

I'm curious to know, where is that setting? Is that Allow App Installation, because I can't seem to find anything that says disable service worker. I really don't want to disable Allow App Installation.

I might take a stab at the service worker myself, but if you've already spent hours then I'm not sure I would be much more successful.

Never mind, I can see that it uses Workbox.

So Workbox does expose fetchDidSucceed and fetchDidFail on routes.

So you need some kind of auth plugin for workbox - Claude suggested a possible solution that might be worth looking at?

https://claude.ai/share/c7d7d3b2-ca9d-4d9f-94f4-a036156e1c99

I'm trying this change now, and with 15 minute session length in Cloudflare, I haven't reproduce the issue yet.

I admit this change was done with the help of Opus 4.6, and I don't exactly know if there's any major downsides of this.

1 Like

Alright, a bit of update here. It turned out I wasn't actually testing my fix, because the old sw.js was cached in /data/node_modules/@flowfuse/node-red-dashboard. So had to delete this, and re-install my own build of the dashboard node. Not entirely sure why it seemed to work regardless, but I suspect the session didn't really expire after 15 minutes as I thought.

Anyway, an easier way to trigger this is to revoke the tokens in Cloudflare console, as it's immediate. Now with the new service worker in place, I've verified that after installing the PWA, and revoking the tokens, a reload of the page redirects me to the Cloudflare sign-in page. I think this is the ultimate verification that the fix indeed solves my problem.

It's my understanding that the main difference is that page navigations now, instead of reaching the cache, goes over network. I can personally live with that over having to reinstall the PWA on a monthly basis.

1 Like

Is your built fork available on github for us to install to try it?

I have not deployed it to npm, so you would have to build a version yourself using my branch.

The change is here:

I run my Node-RED instance in docker compose and build the image with a custom Dockerfile.

Here's the relevant snippet of my Dockerfile.

# Skip the official node for now
#RUN npm install @flowfuse/node-red-dashboard@1.30.2

# Clone, build, then install the patched dashboard
RUN git clone --branch fix-sw-auth-proxy https://github.com/pakerfeldt/node-red-dashboard.git /tmp/node-red-dashboard
WORKDIR /tmp/node-red-dashboard
RUN npm install && npm run build
WORKDIR /usr/src/node-red
RUN npm install /tmp/node-red-dashboard

Then on my phone, I had to unregister the cached service worker by connecting the phone to my Mac, use Safari to open Dev Tools and inspect the tab running my dashboard, and then run this in the console:
navigator.serviceWorker.getRegistration().then(r => r.unregister()).then(() => location.reload())
To force the new version to be downloaded. Then I could install it as PWA again.
This last step of ensuring the new service worker gets downloaded is probably doable with other means as well. Like making sure you clear the entire Safari cache.

OK, I know how to build it. I will give it a go later.

First I am running the original again installed as PWA to make sure I still see a problem.

Unfortunately. The fix was incomplete. It seems as if revoking tokens explicitly, and waiting for one to expire, doesn't really trigger the same behavior. As I was returning to the PWA after a while, I got the error screen in Dashboard 2 again. So I'm exploring more changes now, will report back later.

1 Like

My first attempt had the problem that revoking the tokens manually in Cloudflare seem to work, but as the token expired, redirection did not work. I then had a few follow-up attempts and landed in what's currently on this branch. For some reason, this change does not work when revoking the tokens manually. In this case, the page ends up in a redirection loop, that can be escaped by clearing the cache. But more importantly, the change seems to properly handle when the token expires which ofc is more crucial.

I've had it expire two times in a row, and in both cases I was successfully redirected to Cloudflare's sign in page. I think this works the way I would expect it.

Seeing that it's unfortunate to run a fork of Dashboard 2.0, I would love to hear @knolleary comments on this, to see if we can get this upstream somehow.

Great work! Though I think that you need to put something in to prevent the redirection loop as that is quite devastating to users - it can crash the browser in extreme cases I think.

Can't you put a check in to see if a 301 is going to the same URL on the 2nd time around?

Detecting redirect loops in the SW would require intercepting navigation requests, which is what caused issues with multi-step auth flows. I think the browser's built-in redirect loop protection already handles pathological cases. For example, Safari errors out after 20 attempts.

The approach here is deliberately minimal: remove the SW from the navigation path entirely and let the browser handle auth redirects natively.

I've created an issue and a PR and I hope a discussion on how to solve this upstream can continue there.

I don't know if this is related or not, but for awareness, recently changes were done in iOS/macos - it will no longer allow websocket connections over ip addresses - it requires a DNS name. If the dashboard connects over local ip, it won't work.