Open Julusian opened 11 months ago
This would work as after filling in the 'automatic' fields in the settings, an 'authorize' button will become enabled. Among these fields, would be a field showing the current callback url, which would be dynamic and use the same ip/hostname as the current browser window. Clicking that will open a modal hosting an iframe which performs the auth. The callback url can point to a simple page of ours, which can emit an event to report that the auth has finished, and will push the final tokens out to the still open settings panel. Then the user can hit save and hand over the tokens we retrieved to the module.
Maybe we should also have a 'lighter' flow for this, where we let modules create these buttons without all the config fields, and we store the whole blob of json returned in a value. That way if there is an oauth like flow that doesn't fit what we have defined, modules can still do it themselves with our help to just help collect and store the tokens
For my needs (utilizing multiple different OAuth flows for several different services) I think module developers themselves can handle many steps of each auth flow, but where work needs to be done is specifically on the Redirect step.
Some of the difficulties Companion faces with OAuth
Possible ways to solve these difficulties
$(internal:origin)/instance/${instance.label}/redirect
, where the modules own config code would format that to $(internal:origin)/instance/SomeLabel/redirect
, and the Web UI itself would replace the origin to display protocol://domain:port/instance/SomeLabel/redirect
https://bitfocus.io
domain. Once redirected to that server the user could be then redirected back their Companions origin along with the token or code depending on flow for the module to handle (this can be achieved through OAuth State).These difficulties/ideas are also ordered in difficulty to implement. The first one still requires users to make their own credentials, in some cases such as Google they'd get warnings on redirect about the app not being verified, but it would simplify at least one step of the process. The second may not be as important, as I'm not sure how many services would use the Implicit flow in this situation but for completeness it may be useful to have a solution that can be utilized across multiple modules.
The third point is one I think could be the most professional solution, as then we could have a Bitfocus Companion app for Google Sheets that all users auth to, and it could be verified so wouldn't have the warnings a user gets when using credentials they've made themselves. It would require an external site though, such as the bitfocus site, to achieve. This also doesn't solve all OAuth flows, for example Discord will always require user-submitted credentials as they refuse to verify my Companion app like they did with Elgato's Discord plugin for StreamDeck, but for most modules it could be a benefit.
Yes, the main difficulty is the redirect step. But starting the authorization flow is also something that we should aim to improve. Today there are a few approaches taken:
open
after the user clicks save. This often results in broken modules on companion-pi/headless systems, only works if you are accessing companion on the local machine, and can result in browser windows opening at times you dont expect.None of these options are friendly to users.
I did create https://github.com/bitfocus/companion-oauth/tree/main / https://bitfocus.github.io/companion-oauth/callback/ some months back, which was aimed to help solve this in a minimal/non-invasive manner (no core or module-api changes). But this may not work for everything, and could be improved as it is a first draft and could do with better support for multiple companion installations. Usage is described in https://github.com/bitfocus/companion-module-base/wiki/OAuth.
While I would like to explore the idea of having this as a field type, to make it really easy to do oauth, I am happy to start by implementing small chunks of this which can be used by modules to do parts of the flow manually, and then eventually also be used by this all in one field type.
So perhaps the first step could be to have a new config field type that will allow for adding an 'authorize' button in the config.
This could either call a new module method to generateAuthorizationUrl(.....)
or perhaps it should use the expression syntax to do client side string interpolation. And another expression to check if the button should be enabled.
Then as I proposed above:
Clicking that will open a modal hosting an iframe which performs the auth.
The callback url can point to a simple page of ours, which can emit an event to report that the auth has finished, and will push the final tokens out to the still open settings panel.
It needs some thought on how to parse/fetch the token out of the iframe, considering it could be in the hash or url. (could it be under different parameter names?) I am hoping that we would be able to use https://bitfocus.github.io/companion-oauth/something to be the redirect, and set the iframe/page to allow cross-origin to behave in an 'insecure' manner so that we can extract what we need without needing to redirect back to companion itself.
I think it is worth at least prototyping this to see how it would work in practise (from a UX perspective)
Having a static redirect URL used by all users of a module would allow for creating proper Companion branded apps, rather than end users needing to make dev accounts
I think this would be good, but we should be careful to ensure that modules wont become unusable because the tokens are no longer valid. Because that could happen at any time, including on a show day for someone. And if they are using an older release, being forced to update companion to resolve this will be incredibly annoying or unrealistic as it could require them to rebuild chunks of their config
First off I think an iframe should be ruled out, as while the OAuth 2.0 spec doesn't strictly prohibit the use of iframes it strongly encourages OAuth providers to use X-Frame-Options
to deny it, which is why most OAuth providers (Twitch and Google to name a couple off the top of my head) block auth pages from being loaded inside an iframe.
It needs some thought on how to parse/fetch the token out of the iframe, considering it could be in the hash or url. (could it be under different parameter names?)
If a service is compliant with the specification then the implicit flow would redirect with an access_token
in the URL hash, and for the Auth Code flow it'd be a code
in the querystring params.
I think this would be good, but we should be careful to ensure that modules wont become unusable because the tokens are no longer valid. Because that could happen at any time, including on a show day for someone. And if they are using an older release, being forced to update companion to resolve this will be incredibly annoying or unrealistic as it could require them to rebuild chunks of their config
I don't see anything that would require a user to update their Companion to get new tokens or anything like that. OAuth tokens expire and need refreshing periodically (for example, every hour for Google, 4 hours for Twitch), if a user provides their own credentials then everything could be done in the module to make requests to the service to get new tokens, but that means they have to make their own apps, enable the APIs they need, copy/paste credentials etc... With my suggestion it'd be the module devs making a single app and the credentials and token renewal would be handled outside of the module, with just the access token being passed to the module. This would mean that an outage of that service running outside of Companion would break things, but that has to be weighed against the somewhat amateur nature of the way we're doing things currently needing users to do dev stuff.
One potential idea that just came to mind:
state
param.https://bitfocus.github.io/companion-oauth/callback/
. This would be HTTPS, as some providers require that, that site could then decode the state
param to know where to redirect the user to, and so redirects the user to that URL (which is their Companion web ui), to some path that Companion could use for auth where it takes the params from the URL, passes them to the module (of which it has the instance ID from the state param). and then auto-close the tab.First off I think an iframe should be ruled out, as while the OAuth 2.0 spec doesn't strictly prohibit the use of iframes it strongly encourages OAuth providers to use X-Frame-Options to deny it
Good to know, I wasn't aware of that. I agree that this shouldn't be an iframe then.
I don't see anything that would require a user to update their Companion to get new tokens or anything like that. OAuth tokens expire and need refreshing periodically
Sorry, I meant the client id and client secret.
As part of the generation of the URL, we could encode the module instance id, and origin, into the state param.
Ooh, good idea. For some reason it didn't occur to me to put the current origin into there..
We would also want to include the config field name(s) to write the result into, probably some kind of version number so that we can change the format without changing the url too. But we don't have to define this too much right now, as its not a 'public api' even though it is called externally.
When the token expires, if the user supplied their own app credentials then refreshing a token could be done in module, otherwise it could make the request to an external server run by the module dev or bitfocus to handle token renewal with the credentials stored outside the module.
For now lets leave this as up to the module to figure out.
I'm open to this, but have questions/concerns about scalability (will some providers see 1000s of different client-ids being used from the same ip as spam/bots and revoke tokens or ban accounts? It sounds kinda suspicious, perhaps they could be stolen credentials that are being collected/kept for an attack), or could it simply be against TOS?
It definitely ought to be possible to opt-out of this auto-renew service (or should it be opt-in), some places/people will not like their tokens being held by an external service like this.
This state would show on the Connections screen (which the user would be on after saving the config), and could be clickable and send the user to the service
This will need a bit of thought on how/where to generate the url and get it to the ui (should the second parameter be an object for this state? should it be driven by the manifest.json? something else?).
I have had the thought about maybe we could show a notification that a module needs oauth completing, which this would allow for nicely.
So this approach sounds good to me :)
Is this a feature relevant to companion itself, and not a module?
Is there an existing issue for this?
Describe the feature
Some modules use OAuth to authenticate against an API. This is done in varying ways, due to the complexities involved and it not fitting into our config model very well, as it involves navigating to a URL, and then storing a code that was returned.
The current recommended approach to OAuth is documented on the wiki: https://github.com/bitfocus/companion-module-base/wiki/OAuth
What can we do to simplify/streamline this for modules, to make the experience better for users?
Custom config field helper
A full approach could involve creating a new config field type, which would handle the abstraction of OAuth. It would handle showing/generation of all the necessary config fields for setting up OAuth, and provide them to the module as an object.
with the resulting object looking like:
As part of this component we could offer a button to open the completed
authorizationUrl
so that the user can go through the process.We would also then handle receiving the code through a callback, and converting that into access and refresh tokens.
Some thought will need to go into the UX of this in the browser, and how that will work with saving the module config. But this would allow modules to massively simplify their OAuth usage, and make something that is consistent for the users.
Or keep it simpler with some other options to streamline some small portions:
A way for modules to 'open' a url for the user
Some modules are using open to open the
authorizationUrl
automatically for users. Perhaps we could provide a way for modules to request a url is opened for the user, which we can show as a prompt in the webui. That way it will work for users who are configuring remotely.Usecases
No response