There is the so-called Discourse User API Key mechanism to authenticate external applications for using somebody’s user account in Discourse. We did not use it before because authenticating them is complex given our setup (login to communities.edgeryders.eu, then login to edgeryders.eu, then confirm app access, then return). But if nothing else works, we should try that because it’s the solution prepared for these use cases.
I guess you probably know which solution is more ideal… if that authentication process is only required once per user, it wouldn’t be so bad - although perhaps not intuitive to a user.
another idea would be to have a small server side application with a master API key to create posts on behalf of a user, something like this would not be too difficult to implement:
- user logs in via SSO and is redirected to site
- site has account user ID, calls small server app with ID parameter
- server app posts on behalf of user matching account ID with master API key
it might be another security risk and understand you’re not a fan of more middleware, but it could be the fastest way to get a working solution.
We found this plugin which looks promising and might offer a simple solution to the problem. I’ll install it and check if it works for our requirements.
@Daniel This looks really good - by means of JWT, could a user potentially login from the front end application without redirecting to communities.edgeryders.eu?
Yes, this should be possible.
Ok, so here’s the solution: @owen, please use the off-the-shelf Discourse User API Key mechanism. Use it to let the user approve access of your application to their edgeryders.eu user account. Which means, the URL to forward them to for approving the application’s access is NOT https://communities.edgeryders.eu/...
but:
https://edgeryders.eu/user-api-key/new
This is the default mechanism provided by Discourse for authenticating external applications. We should not try to roll our own alternative for this, because we’d have to maintain that for years, and we already know that all authentication related customizations easily break during Discourse updates. We spent three hours looking for other alternatives like JWT based authentication, but there is no ready-made solution for Discourse anywhere. So we’ll go with what Discourse provides out of the box.
As a result, you’ll have three cases:
-
If the user does not yet have an edgeryders.eu account, your application uses our custom
multisite_account.json
API endpoint as before. -
If the user as an edgeryders.eu account and is logged into it, your application forwards the user to
https://edgeryders.eu/user-api-key/new
for approving access to their edgeryders.eu account. That takes only a single click, and then the user is forwarded back to your application. -
If the user as an edgeryders.eu account but is not logged into it, your application also forwards the user to
https://edgeryders.eu/user-api-key/new
. From there, the user will be forwarded to communities.edgeryders.eu for login, then back to approve the application, then back to your application. This is obviously annoying, but will happen only in 5% of cases as people typically stay logged in.
You can (and should) save the resulting User-Api-Key
value in offline storage into the user’s browser. That way, authenticating the application has only to be done the first time the user uses it.
Thanks @matthias, for the help and work - I will give it a go tomorrow morning.
just to be clear - when I forward to https://edgeryders.eu/user-api-key/new - am I including a url parameter of some kind? how does this work exactly for the redirect…
update - ok just read the documentation page.
The User API Key mechanism will not be simple to implement on the client side. But after looking around long enough, it’s the best solution for people holding an account. So take your time to get this right, and let nobody stress you about it Also, you might find a JavaScript library that already implements the client-side functions of this API key mechanism.
Hello @matthias - I’m still in the process of working on the login function, but I have implemented a directory for the forms with support for different fields.
Could you add these domains
to the whitelist for both edgeryders.eu and http://communities.edgeryders.eu/ ?
Sure. Both done now.
It’s a setting where I have to enter URLs that are allowed as user API auth redirect targets. So I entered these two, hope that’s ok:
http://tell.edgeryders.eu/auth_redirect
https://tell.edgeryders.eu/auth_redirect
Good news - I have the login flow working on the front end, and have tested the post to platform using the User-Api-Key
and it works.
The last issue is with the auth_redirect
parameter. While it works as @matthias set it up, the redirect has to go to the correct url of the form:
For form 14210
this means not to the root domain but Edgeryders Forms
I am not sure if this is possible, as the form urls are dynamically generated. I’m looking into a way of storing the form ID in the payload that is returned instead, and loading the form this way.
The other more minor issue is the json key pair generated on page load - for some reason (way beyond my area of knowledge) it’s fairly slow to generate a key pair in javascript with RSA encryption that works with Discourse. I’m looking to store this as a static value in the .env file to make it faster.
update - @matthias it looks like the “allowed user api auth redirects” setting might support a wildcard in the url, there was a pull request mentioned here. This would solve the first problem if it works.
Good news again
I’ve solved the problem by setting the nonce
parameter as the topic_id
of the form, which is then included in the returned payload and loads the correct form.
It now works, hurrah - http://tell.edgeryders.eu
The last remaining feature to implement is to save the user_api_key
in local storage so users don’t have to re-authenticate again.
In general terms, this is how it works:
- A private RSA key is read from the .env file
- The application generates a public key from this, which is included in a url parameter (along with api scopes, nonce, redirect url) to the endpoint
https://edgeryders.eu/user-api-key/new
- When the user is sent to this url and authenticates, Discourse sends a payload encrypted with the public key back to the application
- The application decrypts the payload containing the user API key (using the private key) which is then used to post the topic to the platform
It sounds a bit simple, but it was a pretty long and windy road to get it working. The upshot of this is it can be used now for many other purposes, we’ve got a working solution for interfacing with the platform from a front end application.
I have also published documentation for anyone who needs to set up a form here.
Great work, that indeed gives us many new options of interacting with the platform!
Yes, this should work as the wildcard syntax is also mentioned in the field explanations. So I have changed the setting now to the following:
https://tell.edgeryders.eu/*
http://tell.edgeryders.eu/*
I think that does not work out as intended. Because I get the following error message with a Server Error 500 on edgeryders.eu/user-api-key after trying to authenticate the application at tell.edgeryders.eu:
ActiveRecord::RecordNotUnique (
PG::UniqueViolation:
ERROR: duplicate key value violates unique constraint "index_user_api_keys_on_client_id"
DETAIL: Key (client_id)=(w02UgniowwfO3sfjbl-pwWwQn0NyJ4xhGi2p_wCD) already exists.
)
My guess is that with the same initial private key, Discourse will see the same client ID, which will be generated from the private key somehow. So I think there is not really a way around the RSA key generation delay, but you can start with that right when the page loads while distracting the user with other things for 5-10 seconds.
Ah I see - yes it works with my account and not yours as I guess the key must be unique. I will test it with the prior solution.
In fact this is not an issue with generating the key pair, but generating a random client_id, so performance should not be affected.
Login to tell.edgeryders.eu with my edgeryders.eu account is confirmed to work now
I’m still not sure that storing the private key is a good idea. It is used to encrypt the data sent later to edgeryders.eu for the User-Api key generation, so “somebody” could listen to that communication and modify it. But then again, if everything is again SSL encrypted via HTTPS, they can’t. But if so, what would this additional layer of encryption exist at all – just for Discourse sites that might not use HTTPS? I don’t know enough about the details of this cryptographic mechanism to make a call, and out of caution would probably not store the key. Or at least ask on meta.discourse.org what the consequences of that would be.
I’m not sure… I think an additional layer of security?
The alternative is to generate a new random key pair each time, but then we run into a technical hurdle (beyond performance): when the user is redirected back to the application a new key pair is generated, so it will be impossible to decrypt the payload since it was encrypted from a previous key pair (on first load). I tested it and it just doesn’t work…
For now it is stored in an .env file, and not in the code itself - so I’m not sure what more can be done to make it secure. But front end cryptography is really not a domain I have much experience or knowledge about…
I propose you look for an answer on meta.discourse.org. If that does not bring new insights, you could also look for an answer in other implementations of the Discourse User-Api key mechanism. And if that does not help, just ask on meta.discourse.org what might be the consequences, security-wide, of storing the initial private key. People there are very helpful Since it’s quite technical, @mention the author of the User-Api key mechanism.
Would be great to get this right so we can rely on this mechanism also when it’s about Discourse accounts with more sensitive data next time.