Skip to content

[2068] Service worker prevents auth redirect when dashboard is behind an authentication proxy - #2213

Open
n-lark wants to merge 2 commits into
mainfrom
2068-sw-auth-proxy
Open

[2068] Service worker prevents auth redirect when dashboard is behind an authentication proxy #2213
n-lark wants to merge 2 commits into
mainfrom
2068-sw-auth-proxy

Conversation

@n-lark

@n-lark n-lark commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Description

Lets a PWA behind an auth proxy follow the proxy's login redirect when the session expires, instead of getting stuck on a cached page. Page loads now go to the network first so the redirect comes through, and fall back to the cached copy only when the network is down, so the app still loads offline and on a cold start.

Builds on @pakerfeldt's #2069: that PR removed the NavigationRoute, which fixed the redirect but left the PWA with no offline fallback, so it hangs on launch when the network isn't up yet, @colinl hit this on Android. Network-first keeps both.

Test plan

Verified manually on Chrome + iOS Safari PWA

All run against a production build (npm run build) served by Node-RED, the service worker only precaches in prod builds. The offline cases just turn the network off (DevTools Offline on desktop, airplane mode on iOS). The auth cases sit the dashboard behind a small stub proxy that redirects to a login page once the session is marked expired, standing in for Cloudflare Access; on iOS that runs over an HTTPS tunnel so the phone can reach it.

Desktop Chrome

  • Service worker installs. Load the dashboard once and confirm the SW is active and has precached the app shell.
  • Loads while offline. With the SW active, go offline and reload. The app still loads from cache instead of showing a blank page or hanging.
  • Redirects to login when the session expires. Expire the session, then reload. The SW lets the proxy's redirect through and the browser lands on the login page, instead of serving the stale cached page. This is the actual bug.
  • The redirect is the handler's doing. Repeat the expired-session reload with the SW unregistered, it still redirects. Getting the same result with and without the SW confirms our handler is passing the redirect through, not that the browser would have anyway.

iOS Safari PWA (over HTTPS tunnel)

  • Redirects to login when the session expires. Expire the session, force-close the PWA, reopen it. WebKit follows the redirect to login.
  • Loads while offline. Airplane mode on, force-close, relaunch. The app loads from cache with no hang on the icon screen.

Related Issue(s)

Resolves #2068

Checklist

  • I have read the contribution guidelines
  • Suitable unit/system level tests have been added and they pass
  • Documentation has been updated
    • Upgrade instructions
    • Configuration details
    • Concepts
  • Changes flowforge.yml?
    • Issue/PR raised on FlowFuse/helm to update ConfigMap Template
    • Issue/PR raised on FlowFuse/CloudProject to update values for Staging/Production
  • Link to Changelog Entry PR, or note why one is not needed.

Labels

  • Includes a DB migration? -> add the area:migration label

@n-lark
n-lark requested review from cstns and knolleary September 8, 2026 17:46
@n-lark n-lark self-assigned this Sep 8, 2026
@n-lark n-lark changed the title [2068] Add navigationRoute that allows for auth redirects with offline fallback [2068] Service worker prevents auth redirect when dashboard is behind an authentication proxy Sep 8, 2026
@n-lark

n-lark commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Hey @colinl - could you test this one on your end please?

@colinl

colinl commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

I will give it a go.

@colinl

colinl commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

No problems specific to this PR on android PWA so far.

I have noticed one thing, that sometimes when returning to the app it hangs forever showing the loading page, but I think that was happening on main. It recovers on swiping down to force a refresh. I need to go back to the current release I think and see if that did it too. It may not be new, it may just be that I am doing a lot of testing with a short expiry time on the cloudflare token and am seeing a rare event. It will take some time to test properly as it is only occurring once or twice or even no times a day. I want to stick with the PR for a few days though to make sure that I don't see lockup I saw on #2069 (#2069 (comment)).

@cstns

cstns commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

I took this for a spin locally to verify both the bug and the fix. Setup: built main and this branch, served each by Node-RED, and put a small stub auth proxy in front that can expire the session and redirect to a login page, standing in for Cloudflare Access. Drove it with real browsers (fresh profile per scenario so each one installs its own service worker), plus an offline pass with the network cut.

What checked out:

  • The bug reproduces on main exactly as Service worker prevents auth redirect when dashboard is behind an authentication proxy #2068 describes. With the SW active and the session expired, a reload never reaches the proxy: the cached shell is served, only the _setup API call hits the proxy and gets a 302 it can't follow, and you end up on "There was an error loading the Dashboard".
  • This PR fixes it. Same scenario on this branch lands on the login page, and the full round trip works: log back in, return to the dashboard, everything renders.
  • Offline still works. With the network gone, the SW serves the cached shell and the app shows its "device appears to be offline" screen. So the regression from removing the NavigationRoute entirely (fix: let browser handle navigation requests to support auth proxies #2069) is avoided, as intended.

One real problem though, and it's subtle: passing { signal } to fetch() with a navigation Request silently downgrades the request mode from navigate to same-origin (this is why workbox passes no init for navigations, see GoogleChrome/workbox#1796). The effect is that the re-issued request arrives at the proxy with Sec-Fetch-Mode: same-origin. I confirmed this in the proxy's logs: every SW-mediated navigation shows same-origin where a plain browser navigation shows navigate. My stub didn't care, so the redirect still worked, but proxies that branch on that header do exist (Cloudflare Access has an option to return 401 instead of a redirect for non-navigation traffic). I ran that variant too: with header checking on and the session expired, the SW build strands the user on a bare 401 page, while a browser without the SW redirects to login fine against the identical proxy. So for exactly the deployments this PR targets, the fix can reintroduce the bug through a side door.

The good news is the clean fix also simplifies the code: workbox-strategies is already in the dependencies (currently imported nowhere), and its NetworkOnly strategy with a handlerDidError plugin is this handler, minus the footguns:

registerRoute(new NavigationRoute(new NetworkOnly({
    networkTimeoutSeconds: 5,
    plugins: [{
        handlerDidError: async () => (await matchPrecache('index.html')) || Response.error()
    }]
})))

I patched the branch to this form, rebuilt, and re-ran every scenario: the strict proxy now sees Sec-Fetch-Mode: navigate and redirects to login, and the offline fallback still works. It also improves the timeout behavior as a side effect: networkTimeoutSeconds races the network instead of aborting it, so a login redirect that takes longer than the timeout can still land, where the AbortController version kills it mid-flight and serves the stale shell.

A few smaller things worth a look while you're in here:

  • The globPatterns switch keys off process.env.NODE_ENV, and Vite only defaults that when it isn't already set. An environment that exports NODE_ENV=development and runs the plain npm run build (the release workflow's command) would silently publish a service worker that precaches nothing. Keying off Vite's mode via the defineConfig(({ mode }) => ...) callback, or pinning NODE_ENV=production in the build script, would close that off.
  • The precache route is registered before the navigation route and wins on exact URL matches, so a navigation to the literal index.html URL is still served from cache and skips the auth redirect. Registering the NavigationRoute first (or not routing index.html) closes it, and makes the "the route below owns navigations" comment accurate.
  • Resolved HTTP errors (a 502 while Node-RED restarts, say) are now passed through as the document, where the old cache-first behavior booted the shell and its self-reloading "server unreachable" screen. Kiosk-style deployments lose that self-healing. If you add a 5xx fallback to the shell, watch out that redirects arrive here as opaqueredirect with status 0, so a naive !response.ok check would swallow the auth redirect again.
  • Unrelated to this PR but noticed while testing: when the server is unreachable while the OS is still online, the app's forcePageReload navigates to the base path without a trailing slash, which falls outside the SW scope, so no offline fallback can help there. Pre-existing behavior, just context.

Nice work on the manual test plan by the way, it was accurate for everything it covered; the header-checking proxy behavior is just hard to catch with a stub that doesn't emulate it.

@cstns cstns left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment above

@colinl

colinl commented Sep 10, 2026

Copy link
Copy Markdown
Contributor
  • Unrelated to this PR but noticed while testing: when the server is unreachable while the OS is still online, the app's forcePageReload navigates to the base path without a trailing slash, which falls outside the SW scope, so no offline fallback can help there. Pre-existing behavior, just context.

@cstns, I don't fully understand the implications of that. What symptoms might that cause on a device running Android?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Service worker prevents auth redirect when dashboard is behind an authentication proxy

3 participants