OpenID Connect Authentication Plugin

official
(David Taylor) #1

discourse-openid-connect

discourse-openid-connect allows an OpenID Connect provider to be used as an authentication provider for Discourse. The plugin aims to provide a minimal implementation of the specification. Specifically, it supports the “Authorization Code Flow”. To get started, follow the plugin installation instructions, or contact your hosting provider.

Our oauth2-basic plugin can be used for connecting to some openid-connect providers (OpenID Connect is based on OAuth2). However, this plugin should require far less manual configuration, and can make use of the JWT “ID Token” if a JSON API is not available.

Configuration is automatically performed using an OpenID Connect Discovery Document. According to the specification, this should be located at <issuer domain>/.well-known/openid-configuration, but Discourse supports any path to allow for non-compliant implementations (e.g. Azure B2C). The discovery document is cached for 10 minutes, to improve performance on high-traffic sites.

If the discovery document includes a userinfo_endpoint parameter, then the plugin will use that to collect user metadata. If not, the plugin will extract metadata from the id_token (A JWT) supplied by the token endpoint. The plugin DOES NOT verify the authenticity of the JWT signature, as this would significantly increase complexity. This decision is supported by the specification:

If the ID Token is received via direct communication between the Client and the Token Endpoint (which it is in this flow), the TLS server validation MAY be used to validate the issuer in place of checking the token signature.

Basic Configuration Options

  • openid_connect_enabled: Enable OpenID Connect authentication

  • openid_connect_discovery_document: OpenID Connect discovery document URL. Normally located at https://your.domain/.well-known/openid-configuration

  • openid_connect_client_id: OpenID Connect client ID

  • openid_connect_client_secret: OpenID Connect client secret

  • openid_connect_authorize_scope: The scopes sent to the authorize endpoint. This must include ‘openid’

  • openid_connect_verbose_logging: Log detailed openid-connect authentication information to /logs. Keep this disabled during normal use.

Advanced Configuration Options

  • openid_connect_token_scope: The scopes sent when requesting the token endpoint. The official specification does not require this.

  • openid_connect_error_redirects: If the callback error_reason contains the first parameter, the user will be redirected to the URL in the second parameter. Used for unusual implementations that send errors in response to user input (e.g. Azure B2C)

  • openid_connect_allow_association_change: Allow users to disconnect and reconnect their Discourse accounts from the OpenID Connect provider

Example setup

Here we will set up the openid-connect plugin to connect to Google’s OpenID Connect provider. This replicates functionality that already exists in the core of Discourse, but it serves as an accessible example.

  1. Head to OpenID Connect  |  Google Identity Platform  |  Google Developers and follow the instructions to obtain OAuth Credentials.

  2. On the same page, follow the instructions to add a redirect URI. This should be https://<your_forum>/auth/oidc/callback (without a trailing slash)

  3. Go to your Discourse site settings and search for “openid_connect”

    • openid connect enabled:

    • openid connect discovery document: https://accounts.google.com/.well-known/openid-configuration

    • openid connect client id: <client-id>

    • openid connect client secret: <client-secret>

    • openid connect authorize scope: openid email (with a space in between)

  4. You’re done. The “Login with OpenID Connect” button will now log in using Google :tada:. These same steps can be applied to other providers, with very minimal changes.

Provider Specific Notes

Please feel free to update this if you find any provider-specific quirks relating to this integration:

Azure AD

Add the email scope, and make sure you’re using the version 2 endpoint configuration document. For example

https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration
Azure B2C

The discovery document URL details can be found here: Web sign-in with OpenID Connect - Azure Active Directory B2C | Microsoft Docs

Yahoo
  1. Head to https://developer.yahoo.com/apps and create a new app

  2. Enter the Application Name, and set the callback domain to your forum domain (e.g. meta.discourse.org)

  3. Under API Permissions, choose Profiles: Read/Write Public and Private. This is the only way I know of to obtain the user email address

  4. Save the app

  5. In the Discourse OIDC settings, set the discovery document to

    https://login.yahoo.com/.well-known/openid-configuration
    
  6. Enter the client ID and secret from Yahoo

  7. Enable the OIDC plugin

15 Likes

Support for OpenID Connect (OIDC)
Custom provider using OpenID Connect via IdentityServer3
OpenIdAuthenticator plugin fails
Removing Yahoo login from Core, and deprecating OpenID 2.0
OpenIdAuthenticator plugin fails
Installing own gem in plugin
Native SSO with Azure AD
Sign in to Discourse using ORCID
Site wont rebuild
User (patron) getting authorization error message
(Klāvs Priedītis) #2

The plugin works great for already registered users.
I’d like to get users signed up using the OpenIDConnect provider exclusively.

Am I correct to observe that this is an issue outside of the scope of this plugin?
Seems to me that Discourse can be set up to use 1 to N login methods, but supports only one sign-up method (either local or sso) even in cases where I disable every other login method except OpenID provider.

What would be the recommended way to allow sign-ups of only those users which are registered to my OpenID provider?

From what I found, I’d need to have an external sso service which would mediate the communication between Discource and my OpenID provider.

Please correct me if I’m wrong in my observations.

0 Likes

(David Taylor) #3

If you “log in” using OpenID connect (or any other authentication provider), and there is no existing account, you will be able to register via that method.

If you disable all other authentication methods, sign up will only be possible via OpenID Connect.

Does that solve what you’re trying to achieve? If not, maybe you can give an example of what behaviour you are expecting?

2 Likes

(Klāvs Priedītis) #4

I think we are on the same page in regards to my expectations.

I just found that the issue I have is
ActionDispatch::Cookies::CookieOverflow on /auth/oidc/callback.

It just happens to affect non-registered users.

1 Like

(David Taylor) #5

I think I know what’s going on here - I’ll have a look in the next couple of days and let you know when it’s fixed.

3 Likes

(David Taylor) #6

The CookieOverflow issue should now be fixed - we now persist the data in the database rather than a cookie:

https://github.com/discourse/discourse/commit/9db829134c6b006c4953015eb7fbca1d0678f2c7

Please try updating to the latest version of Discourse, and try again. (this is a change to discourse core, not the plugin). Let me know if you’re still running into issues.

5 Likes

OAuth2 and Microsoft ADFS
(Klāvs Priedītis) #7

Thanks! It works now!

Now we can get back to the original problem:

Is there a way (without writing my own plugin or implementing Discourse sso service) to prevent users from signing up, except when they already have an account with my OpenID provider?

0 Likes

(David Taylor) #8

Yes, just disable the enable local logins setting in your site settings. That will make the login and signup buttons connect directly to your openid-connect provider.

2 Likes

(Andy Czerwonka) #10

Just wanted to let everyone know that this plugin works great for Azure AD. Just a few things to note:

  1. Make sure you use v2.0 of the configuration endpoint as per the notes above. The default v1.0 endpoint does not support email or profile scope.
  2. If there are issues with the client secret, try generating a new one. We had some issues, but after generating a couple it seemed just fine.
  3. Make sure you turn on “Sign in and read user profile” as a “Delegated Permission” in Azure AD when creating the entry for your site.
  4. Make sure you add the correct callback, which is ‘https://your-site/auth/oidc/callback

Huge thanks to @david for helping us get this working. He responded quickly with a new logging feature that really helped when troubleshooting issues. We might have abandoned it if we weren’t able to see what was going on.

6 Likes

OpenID Connect issue with Azure AD
Discourse-azure-oauth2 -- version token support
Discourse-azure-oauth2 -- version token support
Discourse-azure-oauth2 -- version token support
(Benjamin Miller) #13

Are there any plans on promoting this to a basic or advanced plugin?

0 Likes

(Erik Sundell) #17

Thank you so much for this plugin @david! It is just excellent!

It would be very powerful if I could utilize information in a groups claim to the belonging of groups in discourse, returned by requesting a groups scope.

Example

My OIDC provider (Okta) will if I provide it with an scope of groups return a claim called groups containing a comma separated list of names of Okta groups. I would be delighted if there would be a way to have a way to automatically assign a user to a group based on information returned in this claim.

For example, if you create a new group, you can see the following settings for email domain -> group assignment. I’d love to see a similar system for group assignment.

4 Likes

(Erik Sundell) #18

I was eager to get various things work for OIDC that was only currently available for Discourse SSO, for example the thing about groups above, so I set out to setup a Discourse SSO OIDC Bridge.

I found stevenmirabito (Steven Mirabito) · GitHub had made the following repo:

It is founded on the work by fmarco76 (Marco Fargetta) · GitHub in the following repo:

As I required more features than available in the DiscourseOIDC repo, such as being able to forward groups to Discourse, I kept building on their work to accomplish that. It is at time of writing still untested with Discourse though.

2 Likes

(Sam Saffron) #19

This is a very smart move imo. Our built-in SSO is super rich and keeps growing, this allows you way more flexibility.

That said, I am not against adding more features to the OIDC plugin, its just that there are so many auth plugins and so many features in the matrix it takes a while to add all these things.

3 Likes

(David Taylor) #24

This has just become part of our “advanced plugins” package, and available as part of the Business plan.

7 Likes

OpenID Connect issue with Azure AD
(Louis) #25

Heya, I’ve been struggling with this. I’m trying to use this plugin with WHMCS, but it keeps erroring out on what looks like a valid response to me.

I’ve formatted the JSON and shortened the id_token here so you can read it more easily. Does anyone have any pointers?

(oidc) Authentication failure! invalid_credentials:OAuth2::Error,
{  
   "access_token":"f04e9927ed91b0af56c17550bb0573c58bd7da3b",
   "expires_in":3600,
   "token_type":"Bearer",
   "scope":"openid email profile",
   "id_token":"eyJ0eXAiO.........JfCIw"
}
0 Likes

OpenID Connect issue with Azure AD
(David Taylor) #26

Can you try enabling openid_connect_verbose_logging, and see at which stage the process fails?

1 Like

(Louis) #27

Of course!

0 Likes

(David Taylor) #28

Is there any more information under the error? There should be more detail in the “info” tab, and in the “backtrace” tab

0 Likes

OpenID Connect issue with Azure AD
OpenID Connect issue with Azure AD
(Louis) #29

Info tab is what I pasted above, this was in the backtrace tab:

/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/logster-2.1.2/lib/logster/logger.rb:110:in `report_to_store'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/logster-2.1.2/lib/logster/logger.rb:101:in `add_with_opts'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/logster-2.1.2/lib/logster/logger.rb:52:in `add'
/usr/local/lib/ruby/2.5.0/logger.rb:545:in `error'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:163:in `log'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:486:in `fail!'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-oauth2-1.6.0/lib/omniauth/strategies/oauth2.rb:78:in `rescue in callback_phase'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-oauth2-1.6.0/lib/omniauth/strategies/oauth2.rb:66:in `callback_phase'
/var/www/discourse/plugins/discourse-openid-connect/lib/omniauth_open_id_connect.rb:97:in `callback_phase'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:238:in `callback_call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:189:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:169:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:192:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:169:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:192:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:169:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:192:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:169:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:192:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:169:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:192:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:169:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:192:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/strategy.rb:169:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/omniauth-1.9.0/lib/omniauth/builder.rb:64:in `call'
/var/www/discourse/lib/middleware/omniauth_bypass_middleware.rb:30:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/tempfile_reaper.rb:15:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/conditional_get.rb:25:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/head.rb:12:in `call'
/var/www/discourse/lib/content_security_policy/middleware.rb:12:in `call'
/var/www/discourse/lib/middleware/anonymous_cache.rb:214:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/session/abstract/id.rb:232:in `context'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/session/abstract/id.rb:226:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.2/lib/action_dispatch/middleware/cookies.rb:670:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.2/lib/action_dispatch/middleware/callbacks.rb:28:in `block in call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/activesupport-5.2.2/lib/active_support/callbacks.rb:98:in `run_callbacks'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.2/lib/action_dispatch/middleware/callbacks.rb:26:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.2/lib/action_dispatch/middleware/debug_exceptions.rb:61:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.2/lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/logster-2.1.2/lib/logster/middleware/reporter.rb:30:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.2/lib/rails/rack/logger.rb:38:in `call_app'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.2/lib/rails/rack/logger.rb:28:in `call'
/var/www/discourse/config/initializers/100-quiet_logger.rb:16:in `call'
/var/www/discourse/config/initializers/100-silence_logger.rb:29:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.2/lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.2/lib/action_dispatch/middleware/request_id.rb:27:in `call'
/var/www/discourse/lib/middleware/enforce_hostname.rb:17:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/method_override.rb:22:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.2/lib/action_dispatch/middleware/executor.rb:14:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/sendfile.rb:111:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-mini-profiler-1.0.2/lib/mini_profiler/profiler.rb:171:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/message_bus-2.2.0/lib/message_bus/rack/middleware.rb:57:in `call'
/var/www/discourse/lib/middleware/request_tracker.rb:182:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.2/lib/rails/engine.rb:524:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.2/lib/rails/railtie.rb:190:in `public_send'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.2/lib/rails/railtie.rb:190:in `method_missing'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/urlmap.rb:68:in `block in call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/urlmap.rb:53:in `each'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/urlmap.rb:53:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/unicorn-5.4.1/lib/unicorn/http_server.rb:606:in `process_client'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/unicorn-5.4.1/lib/unicorn/http_server.rb:701:in `worker_loop'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/unicorn-5.4.1/lib/unicorn/http_server.rb:549:in `spawn_missing_workers'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/unicorn-5.4.1/lib/unicorn/http_server.rb:142:in `start'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/unicorn-5.4.1/bin/unicorn:126:in `<top (required)>'
/var/www/discourse/vendor/bundle/ruby/2.5.0/bin/unicorn:23:in `load'
/var/www/discourse/vendor/bundle/ruby/2.5.0/bin/unicorn:23:in `<main>'
2 Likes

(David Taylor) #30

Ok, from that we know that it’s failing here, but that’s not too helpful, because we don’t know what’s causing it to get to that rescue.

By inspecting the error, the response from the token URL seems valid, so it’s weird that its throwing an error. What we can’t see, are all of the headers that go along with that response. My suspicion is that the server is responding with a JSON body, but NOT with a JSON content type. That would cause an error to be thrown here. The acceptable list of JSON content types is here.

Unfortunately I don’t have any way to know for sure, without access to a live WHMCS installation.

5 Likes