Some time ago we have announced version 2.0.0 of the dropbox node, which allowed to get a refresh token from Dropbox. Such a refresh token has a long life, and can be used to obtain periodically new access tokens.
This was needed because Dropbox recently has changed its security policy.
I had tried to make this new authentication flow as user friendly as possible. Not an easy task, because more security results mostly in more complexity...
To do that, I used a redirect url. Summarized: you allow Node-RED to access your dropbox folders, via two sequentially dropbox popup screens. The result is an access code that I did send to Node-RED automatically (via the redirect url), so I could use this access code to get a refresh token from Dropbox automatically.
That worked fine. But Dropbox has two requirements for that redirect url:
It has to point to localhost, which only works when your browser is runned on your host where Node-RED is running. This is not always possible.
Or it needs to be an https url, and the certificate needs to be signed by a trusted CA (e.g. Letsencrypt). So self-signed certificates are not allowed.
These requirements make it impossible to use it for some users. Since I cannot find a decent solution, I want to get rid of that redirect url. But then more manual steps are required. Would like to have some feedback how we can make this as user friendly as possible.
When I don't pass a redirect url to the Dropbox Js SDK, there will be an extra third Dropbox popup screen. So my custom screen (containing the refresh token) is now replaced by a Dropbox screen (containing the access code):
In step 6 we ask the user to manually copy the access code (from the third dropbox screen) into a new "Access code" input field.
In the new step 7 we ask to press a "generate" link to generate a refresh token (based on the entered access code).
Now I don't know how to move this new refresh token to the existing "Refresh token" input field (on top of the config screen).
Should I move the refresh token automatically to that field? Then we avoid an extra manual step, but it is not clear to the user that something has changed. Or we could show the refresh token in a popup (or inside the text of step 7) and tell the user that it has been updated in the "Refresh token" input field.
Should I show the generated refresh token inside the text/popup, and add an extra step in the text to ask the user to copy it manually to the "Refresh token" field at the top of the config screen.
Then we are hopefully rid of all the troubles...
Thanks for reading and digesting this topic!!
Just to be clear... the process described above is usually just completed once, and once a 'Refresh Token' is obtained & stored, it is used by the node to automatically get an access token to allow data to be uploaded/downloaded from dropbox.
The 'Refresh Token' does not expire.
The only times that the above would need to be repeated would be if for example the user changed the 'Permissions' granted to the dropbox app, or if you had revoked the current 'Refresh Token', or if you couldn't restore your system after a catastrophic system failure!!
A pop-up to say that the Refresh Token had changed would be nice, but not sure if it's essential, because they will have completed the preceding steps to do exactly that.
Could the Refresh Token box be moved from it's current location to after 7) on the list, so that it reads in order?
That was also one of my first thoughts. But wasn't sure about it because:
I don't know any other nodes that have textual steps with input fields in between.
The two fields that are currently at the top of the config screen are persistant fields (i.e. which are stored), while the access code is not persistant: since the value is only used once, i.e. a temprary value. So perhaps it makes more sense to keep them separate? On the other hand by placing the edit fields in the text of the corresponfing text, then it is much easier to follow the wizard. Not sure...
Hmm I am now thinking whether it would be possible to grab the access code from the popup window automatically. Then the "Access code" input field would not be required, so no input fields inside the textual steps...
Yes but I am not sure if my above proposal is the brightest crayon in the box...
Uncaught DOMException: Blocked a frame with origin "http://xxx.xxx.xxx.xxx:1880" from accessing a cross-origin frame.
So for security reasons the browser does not allow us to get the access_code automatically from the third Dropbox popup window. So then the user will have to copy it manually. Cannot do much about that...
I like the Or we could show the refresh token in a popup (or inside the text of step 7) and tell the user that it has been updated in the "Refresh token" input field. idea if the user needs to be able to copy & save elsewhere this token (no idea if that may be a useful option) otherwise just say in the text that this has happened.
As far as I am concerned anything you can do to resolve getting the token is a bonus.
One question, can this token be used on other instances of Node-red on another device?
install that in your local http server, e.g. express
assuming your http server is at IP address 192.168.3.46, you point your browser at 192-168-3-46.my.local-ip.co and voilà, you have a bona-fide HTTPS connection using a Let's Encrypt cert!
There's even Node.js code on that web page...
Note that the wildcard cert expires after about 3 months, so you need to re-download periodically if you want to keep this going, but it sounds like that wouldn't be an issue here since it's more of a 1-time thing.
Although it is not a common way of designing a Node-RED config screen, I am rather pleased with it. I think it is more easy to understand for a user which fields he need to fill in at every step.
The only thing I find a bit confusing, is that only the last field "Refresh token" will be stored. The content of all other fields will be gone when you leave this screen and return back. Because I don't want to create an entry for hackers to access Dropbox via Node-RED.
But I haven't finished it yet, because the last step (getting a refresh token with the generated access code) gives me an error for some reason. Note that for this extra step I also had to add a new input field "App Secret" on the screen, since I need that for this last step.
I was afraid that this new way of working would be more complex to the user. But I think it is easier to understand, because you don't need to enter the redirect url's anymore in your Dropbox account.
That is a very good question!
I don't think you can do that currently. Because you cannot share config nodes between Node-RED instances (I think). And the token is stored in the Node-RED cred json file.
I could create a "Copy token" button. But since the token is not available in the frontend anymore after a deploy, that means I would have to create an endpoint to get it from the backend. But that seems a huge security hole to me
Perhaps anybody else has a proposal for your question...
Damn you @tve with all your brilliant ideas
If I only had known this a few months earlier...
You have opened the well-known can of worms again, which I cannot solve now.
But the issues with the Dropbox node expose (again) the fact that we are not able to coope with security requirements from other companies.
Although your tip could be a "workaround" for the Dropbox node users, I would like to discuss in a separate topic how we can integrate your proposal in a general solution. Instead of me trying here again to put some cast here and there to seal our ship...
But first I want to get rid of this Dropbox stuff.
Thanks for the tip!!!
I think it would need a bit more examination of the details as well. Not only does the author say that the service might disappear at any time, it isn't clear to me on first reading just how secure it would be.
The unfortunate truth is that the use of a powerful tool like Node-RED comes with some overheads and considerations and I believe it is important that we continue to try to get to a position that makes it easier for beginners to take a secure stance. There will undoubtedly be several ways of achieving that but taking shortcuts might not always be wise.
But would like to have some feedback about two things:
Question 1 - compose authentication url on backend or frontend
Currently - like in the 2.0.0 version - the frontend calls an endpoint, that generates the Dropbox authentication url in the backend. That endpoint calls the getAuthenticationUrl of the Dropbox instance. That works fine and is very simple, but since I have removed my two other endpoints I thought that the code 'might' be easier to understand/maintain if I could also remove this endpoint. When I compose the authentication url in the frontend code, then everything about oauth2 happens in the frontend code.
However then I need to call the Web Crypto API digest function, which is only available when https is being used. But that won't always be the case, since I am implementing this new version for folks that don't use SSL...
Which means I need to implement a digest function myself in the frontend, e.g. by using this one.
Summarized: should I keep the endpoint that calls getAuthenticationUrl of the Dropbox SDK, or should I implement this function myself in the frontend (inclusive a custom sha256 function)?
Question 2 - store the client id as credential or plain text
You need to create an application in your Dropbox account, and that application has a.o. an app id (= client id). In version 2.0.0 I had stored the client id in the dropbox node credentials, to store it in a safe way.
However in my new implementation, I need the client id in the frontend code. However once you have deployed the node, the client id credential will be removed by Node-RED in the frontend (i.e. it will get a value "PASSWD"). Which means I cannot use it anymore in my frontend code.
So I have a few options:
The cleanest solution would be to store the client id as plain text property, instead as a credential. Because it seems that the client id can be exposed public, so there was no need to store it in the node credentials. Then the client id is plain text, and will be usable both on frontend and backend.
However then I (again) would have a breaking change, because in existing flows the client id is stored as a credential and not available on the client side.
I can keep using the client id as a node credential, but then I would need to provide an endpoint that can fetch the client id from the backend. Which would be very silly.
I introduce an extra endpoint again and move my frontend code to the backend, where I know the client id. But that makes the code again more difficult to understand.
The cleanest solution would be option 1, but that would be the second breaking change in a row...
Does anything get sent between Dropbox and the client that is sensitive? If so, shouldn't HTTPS be enforced? If Dropbox is trying to send something over the Internet that shouldn't be intercepted then HTTPS certainly should be enforced and you aren't doing anyone any favours by trying to avoid that.
Of course, perhaps nothing is being sent from Dropbox. In that case, it would be a moot point. But I'd want to confirm that by reviewing the data sent between the client and Dropbox and back.
I don't know any of the details of the Dropbox calls I'm afraid. However, what I would say with confidence is that rolling your own security is pretty much NEVER a good idea.
BTW I had forgotten to thank my pall @Paul-Reed for his assitance behind the scene. Without his examples and mental coaching, I most probably would have jumped out of the window
No no, between the node and the Dropbox server is all SSL. That communication is started via the Dropbox authentication url that I must compose.
The problem is that some users don't have SSL setup on their Node-RED system. Or they have SSL but with a self-signed certificate. In both cases you can't specify a Dropbox redirect_url, which I had used in my previous version (which is an easy way to return the refresh token back to Node-RED). Therefore I am implementing a new version without redirect url.
Well I have completely nor time nor energy nor intentions to setup my own security. I just need to have an url to contact Dropbox to start an OAuth2 authentication flow:
I can call the getAuthenticationUrl (of the dropbox SDK) on the backend via a custom endpoint, which will be called via http for some users...
Or I can compose that url myself on the frontend size. Then I don't need a custom endpoint anymore, and don't need to send data over http, and all Oauth2 related code is on the frontend size.
However to generate the your_random_code_verifier I need to create a sha256 hash of a random number. In the NodeJs backend that is very simple, but in the frontend you need to call that Web Crypto API digest function. But that function is not available in a frontend that is connected via plain http to Node-RED. So it will not work for users that don't use SSL for Node-RED. Which makes no sense in my case, because I am going to send that hash via a https link to Dropbox anyway.
So I can solve that by adding that custom sha256 function to the dropbox node html file, but not sure if people like e.g. @dceejay think it is ok to pollute the html with this kind of code...
The first two are HTTPS and we assume they're secure enough. The last one may not be HTTPS and thus hackable.
The question is: if an attacker hacks the communication between the browser and the node-red runtime can they gain access to your dropbox account or can they cause the runtime to access a different dropbox account (e.g. with malicious content) while you think it's yours? (There likely are more threats, but I think this is enough.)
If you can provide a link to the dropbox docs on the auth protocol you're using I can take a look, but if you really need to send the type of data across the insecure link that you talk about I would be surprised if the answer wasn't "yes, an attacker could get access".
However, OAuth is designed for there to be no link between the browser and the service trying to get access (other than a "start here URL"), so I'm a bit surprised...
You are absolutely correct.
The oaith2 is between the flow editor and dropbox, wichh returns a refresh token and is secure. Then I store the refresh token in a node credential, which should be secure also. Of course if you use http for your flow editor, then it can be intercepted. But I assume that users only do this within their LAN behind a firewall.
This version I avoid Dropbox redirect url's to make sure it also works within a secured LAN. Cannot expect that everybody uses LetsEncrypt certificates on all their Raspberies.
But of course when they use plain http for Node-RED over the internet, then things can go wrong.
But that is a general problem, apart from this node. When you do it like that, things will go wrong eventually in some way or another...
That is indeed what I meant by "But of course when they use plain http for Node-RED over the internet, then things can go wrong". But that is application level stuff for me, not node level. Like HomeAssistant does.
So allthough you guys are right, I would like to keep that for a separate discussion. Otherwise nobody is going to read this whole discussion anymore and answer my above two questions. So that I can finish this node, and start on my long backlog...
Unfortunately, I don't understand the technical issues being described but I agree with the above 100%. If initial registration is a little more complicated and dependent on me, that's fine if I only have to do it the first time. I appreciate all of the hard work.
Yes that was my idea also. The whole OAuth2 stuff is not easy literature, and I would like to keep the code as simple as possible. To make sure that more people can contribute fixes in the future, to keep this node alive for the community...
Of course the previous version was 2.0.0 due to the breaking change. Not sure if I need to use then version 3.0.0 since there is another breaking change.
Will explain a bit more in detail about the breaking change:
The app key is now a credential, i.e. is stored secure in Node-RED and you can't see the value:
The app key (= client id) will now become plain text, which is not a security issue (see here). That allows me to have the client id available in the frontend, where all the OAuth2 will be located from now on for simplicity.
But that means that in the frontend side the App key field will become empty. I cannot simply convert the old value from the credentials automatically to a plain text value, since the App key field currently does not contain the value that you have entered (but instead it contains __PASSWD__).
So when you want to request a new refresh token, you will have to get the App Key again from your Dropbox account.
Although when thinking further about this, is not really 100% a breaking change:
When you want to request a new refresh token, you need to enter the App Key again. However that is not really a big issue, since you also need to enter the App Secret. So you need to login to your Dropbox account anyway.
P.S. The App Secret is a non-persisted field. I only use it to get the Refresh token, but it is not stored in Node-RED for security reasons. Don't want to screw up anybody's Dropbox account...
I could keep the old value in the credentials. That would be a few lines of legacy code, but then I could do var clientId = node.clientId | node.credentials.clientId. Which means I use the plain text client id, but if that is not available I try to use the old clientId from the credentials. So as long as you would not get a new refresh token, the old App key (= client id) could still be used...
Well this is just a matter of favorite flavour. I need to calculate the https url to access Dropbox for a new authentication flow. Can do that in two ways:
Or when you want to have all the OAuth related stuff in the frontend, then I need to remove the ajax call and endpoint. And then I need to implement the code myself in the frontend html, to generate the authentication url myself (similar to how the getAuthenticationUrl would do it on the backend). It will be around 20 lines of very cryptic code. So no big deal I think. However the problem is that I need to calculate a sha256 hash then in this frontend code. Cannot use the Web Crypto API digest function, because that is not available when people use a http connection to their Node-RED flow. So I need to add a custom function (like this) to the html file, to calculate it myself. But not sure if this is allowed in a semi-core node, because it can be considered as polluting the html file...
BTW the Web Crypto API does not offer this digest function (to calculate hashes) when using a http connection (to Node-RED in this case), because when you would send the calculated hash across this unsecure connection then it can be intercepted. However I am not going to send it to Node-RED across the unsecure http connection. Instead I use it in the dropbox authentication url, which is a secure https url. So I don't think a security risk would be introduced by generating the hash myself. But if anybody thinks I am wrong, please let me know!!
Although I need to be a bit more clear about the endpoint. Suppose a user has an unsecure http connection to its Node-RED instance:
The frontend does an ajax call to the endpoint to calculate an authorization url, and it passes the client id (= App Key) to the server. As mentioned above, that should be no security issue.
The called endpoint will calculate an authentication url and return it to the frontend. That url contains - besides the client id - also the code_verifier.
If somebody intercepts the response, he can get hold of that code_verifier. Which means it can be abused. Although the security risk is not very large, since that code verifier can only be used once (see here).
But to be honest I think it would be better to remove the endpoint, and do all the OAuth2 stuff in the frontend code. Then I don't need to send anything to Node-RED about OAuth2. Think that will be a better solution. A bit more code, but less (possible) vulnerabilities seems to me. But of course I am not a security expert.