zwave-js / zwave-js-ui

Full featured Z-Wave Control Panel UI and MQTT gateway. Built using Nodejs, and Vue/Vuetify
https://zwave-js.github.io/zwave-js-ui
MIT License
938 stars 200 forks source link

ZWaveJS fails to load when running with external auth due to service worker #3427

Closed ajacques closed 7 months ago

ajacques commented 9 months ago

Checklist

Deploy method

Docker

Z-Wave JS UI version

9.3.2

ZwaveJS version

12.3.0

Describe the bug

I'm using an external auth provider (nginx + vouch proxy) to protect all of my services with one SSO solution. I choose to do this instead of having separate username and passwords for different services. Effectively how it works is it checks for a cookie on all requests to ZWaveJS and redirects the browser to the login page.

The problem is that ZWaveJS now appears to use a service worker and works as a PWA (I suspect this came in with this commit d409051b9e96939fa0510399e45693b331a34cf1) which intercepts the call to / and causes this to fail. When the authorization expires, I have to hard refresh the page to load it.

What happens is the / page loads from the Service worker, then the GET /api/auth-enabled call gets a 302 Found to redirect to the login page, the app doesn't know what to do and fails to load the app. The / request does not hit the backend app (I've checked my logs), so it can't know that / is failing. The only way to bypass this is to hard refresh the page, but that's not easy on mobile phones.

To Reproduce

  1. Install ZWaveJS behind some kind of reverse proxy such as nginx
  2. On the reverse proxy, enable an auth proxy that intercepts requests, such as vouch proxy
  3. Try to load the ZWaveJS and see that it gives a "Network Error. Retry" and the page fails
  4. Hard refresh the page (ctrl-shift-r) and notice the app loads because it bypasses the service worker for GET / and the app will work until the auth token expires.

Expected behavior

I expect the ZWaveJS application to be able to handle when there's a separate auth provider and redirect to the login page. ZWaveJS does not have to specifically have to support these providers, just handle the redirects.

I see two options here:

  1. When the service worker receives the GET / request, it revalidates with the service to see if the response includes a redirect or not. This can also be used to refresh the cache in the service worker. (basically cache-bust the / call because the app can't work without internet anyway)
  2. When the app sees a 302 redirect for GET /api/auth-enabled, it follows the redirect in a full frame redirect (i.e. browser tab navigates to the new page.) This appears to be the first request that actually gets forwarded to the server so would have to be the first point the app knows there's external auth.

It seems like the service worker comes from Vite, which possibly internalizes the cache handling logic. I'm unfamiliar with what knobs it has to know whether 1 would work, but 2 should be doable in the app side. I tried changing the Cache-Control on GET / to be must-revalidate, but the service worker does not respect it

Additional context

No response

Daniel-dev22 commented 5 months ago

Looks like this regressed in a recent release I'm running. 9.9.1 and I'm getting this when it should be redirecting.

Screenshot_20240315_090100_Chrome

robertsLando commented 5 months ago

@Daniel-dev22 nothing changed on code to cause that AFAIK

Daniel-dev22 commented 5 months ago

@Daniel-dev22 nothing changed on code to cause that AFAIK

This is the only webapp giving me trouble.

I know this was a follow-up to the original pr that fixed this? Not sure if this matters or if anything else could have changed to cause such an error.

https://github.com/zwave-js/zwave-js-ui/commit/366b8dcb056fee10c1d32e9311a8161047f1fe10

robertsLando commented 5 months ago

@Daniel-dev22 could you try to test the commit before that to see if that is working? If so we should find a way to make both work...

Daniel-dev22 commented 5 months ago

@Daniel-dev22 could you try to test the commit before that to see if that is working? If so we should find a way to make both work...

Hm I tried 9.8.0 and cleared cookies + cache and tried a different browser I never used with zwave js UI to avoid any cache issues and I get the same error which is odd.

It only works if cookies are cleared and I sign in with authelia but once it's time to re-authenticate it fails which is when it needs to be redirected similar to what was happening before the fix was found.

I didn't upgrade authelia either. It's still the same version as before.

robertsLando commented 5 months ago

Hm I tried 9.8.0 and cleared cookies + cache and tried a different browser I never used with zwave js UI to avoid any cache issues and I get the same error which is odd.

That is odd yeah because I just checked and 9.8.0 version is the right before that commit so I don't think that's the root cause.

@ajacques Is it still working for you?

Daniel-dev22 commented 5 months ago

Hm I tried 9.8.0 and cleared cookies + cache and tried a different browser I never used with zwave js UI to avoid any cache issues and I get the same error which is odd.

That is odd yeah because I just checked and 9.8.0 version is the right before that commit so I don't think that's the root cause.

@ajacques Is it still working for you?

I checked the console and I see

SyntaxError: Unexpected token '<', \"<a href=\"h\"... is not valid JSON

Initially when I loaded the page I saw CORS errors for fetching icons not sure if that's even related as I never touched cors in traefik since we last determined this was working.

Access to font at 'https://authelia.nuc.domain.net/?rd=https%3A%2F%2Fzwave.domain.net%2Fassets%2FMaterialIcons-Regular-5743ed3d.woff2&rm=GET' (redirected from 'https://zwave.domain.net/assets/MaterialIcons-Regular-5743ed3d.woff2') from origin 'https://zwave.domain.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Access to font at 'https://authelia.nuc.domain.net/?rd=https%3A%2F%2Fzwave.domain.net%2Fassets%2FMaterialIcons-Regular-11ec382a.woff&rm=GET' (redirected from 'https://zwave.domain.net/assets/MaterialIcons-Regular-11ec382a.woff') from origin 'https://zwave.domain.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Access to font at 'https://authelia.nuc.domain.net/?rd=https%3A%2F%2Fzwave.domain.net%2Fassets%2FMaterialIcons-Regular-29c11fa5.ttf&rm=GET' (redirected from 'https://zwave.domain.net/assets/MaterialIcons-Regular-29c11fa5.ttf') from origin 'https://zwave.domain.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
robertsLando commented 5 months ago

@Daniel-dev22 CORS are enabled on my end because otherwise it wouldn't work even for me when developing, you should check if your proxy actually is blocking cors for some reason?

Daniel-dev22 commented 5 months ago

@Daniel-dev22 CORS are enabled on my end because otherwise it wouldn't work even for me when developing, you should check if your proxy actually is blocking cors for some reason?

Yeah in traefik I only allow using this parameter accesscontrolalloworiginlist for traefik.domain.net, authelia.domain.net and domain.net

https://doc.traefik.io/traefik/middlewares/http/headers/#accesscontrolalloworiginlist

robertsLando commented 5 months ago

Shouldn't you allow also zwave.domain.net?

Daniel-dev22 commented 5 months ago

Shouldn't you allow also zwave.domain.net?

I wasn't sure if all app sub domains should be added to that list or it should strictly be authentication/root domain on the list.

ajacques commented 5 months ago

I just upgraded to 9.9.1 and the login redirect does not appear to be working correctly. It looks like it tried to follow the redirect which means the max redirect limit wasn't hit. I will have to investigate further in the next few days. CORS is not the fix for this. The JS needs to see the redirect and use that to trigger a full frame redirect. Enabling CORS means that the browser followed the redirect and is going to get an unparseable response payload.

Yeah in traefik I only allow using this parameter accesscontrolalloworiginlist for traefik.domain.net, authelia.domain.net and domain.net

FYI, the CORS list is the source origin, not the destination origin. So if you wanted CORS to work, the list would include zwave.domain.net. CORS may work in some cases but not all.

ajacques commented 5 months ago

Plot twist. It is working for me on 9.9.1 correctly. I think the JS was cached from an older version in the service worker cache on the computer I tested on. @Daniel-dev22, if you're saying it's not working, you should be able to hard refresh and get it to login, then does the version in the top right show 9.9.1 or something modern?

robertsLando commented 5 months ago

It could be the service worker failed to refresh the cache for some reason related to the proxy maybe...

ajacques commented 5 months ago

I suppose it is possible that the SW tried to fetch the latest version, then failed because the auth proxy rejected it, but in the above case Daniel-dev22 suggested that it used to work for them, then stopped. In theory, they should have had a version cached. I could see how that could cause my problem if I have a pre-9.8.x cached version, but not post-9.x.x cached JS in the SW cache (which had the new auth check.)

However, that gives me an idea. I'm going to remove /manifest.webmanifest and /assets/* from the Auth proxy and then try to test the next upgrade and see what happens. That should enable the service worker to be able to fetch the latest assets at any point... Though I don't think this is going to work since the /index.html is what actually defines what version of the JS bundle is provided.

Daniel-dev22 commented 3 months ago

I suppose it is possible that the SW tried to fetch the latest version, then failed because the auth proxy rejected it, but in the above case Daniel-dev22 suggested that it used to work for them, then stopped. In theory, they should have had a version cached. I could see how that could cause my problem if I have a pre-9.8.x cached version, but not post-9.x.x cached JS in the SW cache (which had the new auth check.)

However, that gives me an idea. I'm going to remove /manifest.webmanifest and /assets/* from the Auth proxy and then try to test the next upgrade and see what happens. That should enable the service worker to be able to fetch the latest assets at any point... Though I don't think this is going to work since the /index.html is what actually defines what version of the JS bundle is provided.

@ajacques were you able to test your theories?

ajacques commented 3 months ago

Unfortunately, I haven't been able to reproduce the issue since then. I do have /sw.js and /assets/* permitted to bypass the auth check and have been able to go through some upgrades without issues and I think this should be enough. But I haven't confirmed that this fixes it with a negative check (i.e. it doesn't work before making this change.) Testing is slow because I'm running out of releases to test on.

sushain97 commented 2 months ago

👋 I just wanted to chime in and say thanks a bunch to @robertsLando and @ajacques! My setup is Caddy + Auhentik and things seem to work with no errors in network panel after I delete cookies + hard refresh. I didn't test with a stale Authentik session, though.

Here's my config for anyone who drops in:

zwave.mydomain {
    reverse_proxy /outpost.goauthentik.io/* http://authentik-server.idp.svc.cluster.local
    forward_auth http://authentik-server.idp.svc.cluster.local {
        uri /outpost.goauthentik.io/auth/caddy
        copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
        trusted_proxies private_ranges
    }
    reverse_proxy http://zwave.home.svc.cluster.local
}
resource "authentik_provider_proxy" "zwave" {
  name                  = "zwave"
  external_host         = "https://zwave.mydomain"
  mode                  = "forward_single"
  access_token_validity = "days=3"
  skip_path_regex       = "^/manifest.webmanifest$"
  authorization_flow    = data.authentik_flow.default-authorization-flow.id
  authentication_flow   = data.authentik_flow.default-authentication-flow.id
}

resource "authentik_application" "zwave" {
  name              = "zwave"
  slug              = "zwave"
  meta_icon         = "https://zwave-js.github.io/zwave-js-ui/_images/app_logo.svg"
  protocol_provider = authentik_provider_proxy.zwave.id
}

resource "authentik_policy_binding" "zwave-access" {
  for_each = { 0 : data.authentik_user.sushain }
  target   = authentik_application.zwave.uuid
  user     = each.value.pk
  order    = 0
}

The only interesting bit is really skip_path_regex = "^/manifest.webmanifest$" which I added after requests there 302'd to Authentik and then failed with CORS errors.

robertsLando commented 1 month ago

Could someone of you guys confirm me if #3825 could break your setup?

Daniel-dev22 commented 1 month ago

Could someone of you guys confirm me if #3825 could break your setup?

Is there a test build we can run for that pr?

robertsLando commented 1 month ago

Unfortunately nope as it's on a fork, but I can merge it and then you can use master to check.

robertsLando commented 1 month ago

Wait for: https://github.com/zwave-js/zwave-js-ui/actions/runs/10061183639/job/27810599866

ajacques commented 1 month ago

LGTM from an external auth provider perspective.

robertsLando commented 1 month ago

Thanks for your feedback @ajacques ! :)

Daniel-dev22 commented 1 month ago

Works for me as well!

robertsLando commented 1 month ago

@Daniel-dev22 🙏🏼

jaimevisser commented 1 week ago

Can reproduce with Traefik and basic auth as well.