I have an issue running the flowfuse dashboard behind cloudflare zero-trust, using google to authenticate.
When the token expires the dashboard is initially unaffected. It seems that the existing websocket connection continues ok even though the token has expired. That is ok. However, if I switch pages in the dashboard then bits of the dashboard do not appear and in the browser (edge) console I see stuff like
Access to script at 'https://me.cloudflareaccess.com/cdn-cgi/access/login/nodered.mydomain.uk?kid=...&redirect_url=%2Fresources%2F%40colinl%2Fnode-red-dashboard-2-ui-gauge-classic%2Fui-gauge-classic.umd.js' (redirected from 'https://nodered.mydomain.uk/resources/@colinl/node-red-dashboard-2-ui-gauge-classic/ui-gauge-classic.umd.js') from origin 'https://nodered.mydomain.uk'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
GET https://me.cloudflareaccess.com/cdn-cgi/access/login/nodered.mydomain.uk?kid=...&redirect_url=%2Fresources%2F%40colinl%2Fnode-red-dashboard-2-ui-gauge-classic%2Fui-gauge-classic.umd.js net::ERR_FAILED 200 (OK)
TypeError: Failed to fetch dynamically imported module: https://nodered.mydomain.uk/resources/@colinl/node-red-dashboard-2-ui-gauge-classic/ui-gauge-classic.umd.js
Is there a solution to this?
Refreshing the page takes me to the login page, but it would be much better if this happened automatically.
Does this always happen when you switch pages, or only after token expiration?
Do you want your page to go to login automatically and unsolicitedly once the token expires?
I've highlighted this as an issue many times. It isn't specifically a Node-RED issue but rather stems from the fact that websocket connections do not allow custom headers and therefore the server cannot verify if a session token is still valid.
There are ways to work around this, the easiest being to include any authentication/authorisation token in EVERY message that passes between client and server. This is why UIBUILDER has a set of hooks that allow processing like this to be added.
I think this is basically saying that the browser will not allow access to that "foreign" resource origin.
You will need to adjust Node-RED's CORS settings to allow access.
I'm afraid I can never remember how to do this and always have to look up the correct CORS header.
Only if the token has expired. Also I suspect that it may only happen the first time that page is visited. Hopefully it is not fetching the contrib node every time the page is opened.
I suppose that would be the ideal, though personally I don't have strong feelings about it. I suppose that if I can find where the resources are loaded then forcing a page reload when the error occurs would be ok.
I have done such an implementation at a live project, where the flow goes back automatically to login after a timeout (in case of token) or configurable time with no user activity. It also forces going through the login and blocks direct access to the page.
Do you want me to share?
Unfortunately this was wishful thinking on my part. I have moved on from the particular case described above to the more serious one which arises if the auth session expires and then an attempt is made to open the websocket connection. Currently this fails and the code assumes that the server is unavailable and repeatedly retries the connection, which accomplishes nothing. The correct action is to force a page reload.
I have documented this in the issue below, but have not so far found a solution. If anyone who knows anything about such things could have a look at that and maybe offer some ideas that would be great.
Preferably continue discussions on the issue page rather than here please.
When a socket.io connection attempt starts (or a raw ws: connection for that matter), the initial connection is over http(s). This initial connection is the only place that custom http headers are supported (well, for socket.io, the fallback long-polling is http(s) so that supports it too but that is an aside). So what should happen is that the socket.io connection should have the same header processing as the main ExpressJS handler in terms of authentication and proxy related headers.
Sounds like this isn't happening. So I would suggest that this is probably a bug.
What tells you that? I am not saying you are wrong, I am just trying to understand what is going on, we are outside my knowledge base here. The reason the connection attempt fails is that the initial https attempts to redirect to the login page (which is a cloudflare domain) and fails with a CORS error.
Hard won experience! Went through all of this (several times over the years) with UIBUILDER.
UIBUILDER has several places where you can add in custom Socket.IO message processing so that you can hook in authentication and authorisation logic. These hooks for example which you can configure in the uibuilder section of your settings.js file:
It has taken me a LOT of time and angst trying to understand the inner workings of websockets in general and Socket.IO specifically.
Well, it is perfectly possible that I've misunderstood. I suppose that what you are really covering here is what the client does rather than the server? I do get a bit wrapped up with the server logic!
At the very least though, some additional logic is likely to be needed for when a socket.io (re)connection attempt fails.
I don't know how this is managed in the D2 client libraries, I only know how I do it. And truthfully, my reconnection logic - though it works - isn't ideal. Indeed, I currently have a slightly related issue to yours in developing Markweb (single-node website creator in uibuilder). I want to reload the page after a client has disconnected and then reconnected - as happens a lot when your browser or its host goes into sleep mode and then back out. In the uibuilder client library, I expose custom events for this happening but the logic isn't quite right to be able to capture only a reconnection and not the initial series of connections that happen normally (first connection is over http(s), then you may get several long polling connections and finally an "upgrade" connection - which you can see has a 101 response code in the network tab of your browser dev tools).
This issue is, in fact, the focus of tomorrow.
Anyway, sorry for the side-track. Bottom line, you probably need a way to respond to socket.io connection errors in the browser, not sure if they are exposed in D2.
That is what I am trying to fix. I believe I now have the solution.
In the client, before the socket tries to connect, perform a fetch of '_setup' which is a small json file. This is what was giving the CORS error when cloudflare tried to redirect to the login page. The solution is to use fetch('_setup', {redirect: 'manual'}) so that it will not actually do the transfer. Then I can check the response from that and check that the content-type header includes 'application/json'. If it doesn't then I will force a page reload so that the login screen will be shown.
It remains active. There have not been any complaints about that as far as I can remember, thought that does not seem to be the 'correct' behaviour to me.
And it absolutely isn't. That was what I was wittering on about before. When an authentication session expires, there should only be enough communications to allow that fact to be known so that actions can be taken. No general communications, certainly no general data should be allowed at that point. There are a couple of ways to do that. But they aren't especially straight-forward.
The code I described above can be used to detect it, so presumably running that on a polling basis once a minute (or whatever) and reloading the page when it happens would be good enough.
I don't. Because there are too many variables and too many different things people may want to do.
Also worth noting that such a check should NOT be done in the browser since that is not secure.
What I do is to make hooks and middleware options available on the server so that people can wire in checks that suite them.
On the client end, you can check for an expiry timestamp - noting that browsers don't generally have access to HTTP headers (though a web worker can access them) and so you need to do a process on the server and tell the client via a message about session info. The point about the client checks being that they are simply a convenience for users. Not a security feature.
To get per-message session security with websockets, you have to process every in- and out-bound message. Generally, this means keeping track of session info unless you have the info already in web headers. In that case, you have to check every message against the session expiry timestamp and put a hold on messages if the session has expired. Session expiry might be encoded into a JWT or might be in a different header, or might not be available at all! (In which case you would need a further server process to track times).
So now you know why - though I've tried many times over the decade or so of uibuilder's life - I don't make any attempt to try and process this.
I have middleware - which requires a JavaScript file containing some pre-defined functions, a bit of a pain to get right - or the newer hooks which can go into your settings.js. Both have functions for dealing with HTTP(S) and Socket.IO web connections and functions for intercepting incoming and outgoing messages so that you can do the per-message checks and blocks as needed.
Also worth noting that uibuilder uses separate message channels for control messages and user messages. This lets you block standard user messages but still allow control messages.
All very carefully thought through and architected to provide the flexibility required for all manner of requirements.
I think that all says that it can't really be done for a simple tool like the dashboard. At least not in a way that copes with the general case where we know nothing about the auth system in use.