# Adding a new 'managed' authentication method to Discourse

**URL:** https://meta.discourse.org/t/adding-a-new-managed-authentication-method-to-discourse/106695
**Category:** Developer Guides
**Tags:** explanation
**Created:** [January 16, 2019, 1:44pm UTC](https://meta.discourse.org/t/adding-a-new-managed-authentication-method-to-discourse/106695 "2019-01-16T13:44:45Z")
**Posts on this page:** 3
**Page:** 1

<div class="post-metadata">

### Author: ![Discourse](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/discourse/32/148734_2.png) [@Discourse](https://meta.discourse.org/u/Discourse)
#### Post date: [January 16, 2019, 1:44pm UTC](https://meta.discourse.org/t/adding-a-new-managed-authentication-method-to-discourse/106695/1 "2019-01-16T13:44:45Z")

</div>

Continuing from [Future Social Authentication Improvements](https://meta.discourse.org/t/future-social-authentication-improvements/94691)…

We are now in the process of moving all ‘associated account’ information into a single database table. This will help to significantly reduce duplicated logic, and allow quicker development in the future. For example, [migrating our core twitter logic](https://github.com/discourse/discourse/commit/160d29b18a0ff68f0cdc152b9d5f461869190b7e) to the new system reduced the number of lines of code from 136 to just 24 🎉.

This post isn’t designed to be a step-by-step instruction manual for adding a new authentication provider, but it will aim to provide an overview, pointing to the relevant source code where necessary.

## Implementing an authenticator

Each authenticator **must** implement a subclass of [Auth::Authenticator](https://github.com/discourse/discourse/blob/main/lib/auth/authenticator.rb). To use the new shared logic, the authenticator **can** instead extend [Auth::ManagedAuthenticator](https://github.com/discourse/discourse/blob/main/lib/auth/managed_authenticator.rb). An example of a bare-bones implementation can be found in the core Facebook authenticator:

> <https://github.com/discourse/discourse/blob/main/lib/auth/facebook_authenticator.rb>

`name` and `register_middleware` must be overridden by implementing classes, along with `enable_setting` — the boolean site setting an admin uses to turn the provider on.

An authenticator **should** also declare `required_settings`: the site settings that must have a value before an authentication can succeed. The base class uses them for `configured?`, and `enabled?` is `enable_setting && configured?`, so a provider with missing credentials is never advertised on the login page and its `/auth/<name>` route stays closed — otherwise clicking the button strands the user on the provider’s own error page with no way back. Declaring `required_settings` also lets `AuthProviderCredentialsValidator` refuse to enable the provider in the first place; wire it up with `validator: "AuthProviderCredentialsValidator"` on the enable setting.

```rb
def enable_setting
  :enable_google_oauth2_logins
end

def required_settings
  %i[google_oauth2_client_id google_oauth2_client_secret]
end

```

An authenticator that overrides `enabled?` directly opts out of both gates.

> ℹ **Aside:** for multisite compatibility, it is important that any site-specific information is supplied to omniauth in a `setup` lambda, rather than being fixed at the time of definition. See all core authenticators for examples of this.

All logic to link external accounts to Discourse accounts is handled by `Auth::ManagedAuthenticator`. This relies on the omniauth provider returning data in the format defined in [their documentation](https://github.com/omniauth/omniauth/wiki/Auth-Hash-Schema). If any manipulation of this data is required, Authenticators can override the `after_authenticate` method, and manipulate the auth\_token as required. For example, the core Twitter authenticator removes all the `extra` information from the token:

> <https://github.com/discourse/discourse/blob/b46b6e72d1906ca31e29855bda71f3498c8e203f/lib/auth/twitter_authenticator.rb#L10-L14>

Data is stored in the `user_associated_accounts` database table. `provider_uid`, `info`, `credentials` and `extra` are all taken directly from the data returned by omniauth.

> <https://github.com/discourse/discourse/blob/b46b6e72d1906ca31e29855bda71f3498c8e203f/app/models/user_associated_account.rb#L13-L24>

Once an `Authenticator` class has been defined, it needs to be registered. This must happen early in the application’s lifecycle, and can **not** happen within a plugin’s `after_initialize` method. The minimum registration can simply contain a reference to the authenticator. In a plugin, registration can be done using the `auth_provider` function. For example:

```rb
auth_provider authenticator: OpenIDConnectAuthenticator.new()

```

In core, registration takes place in [`discourse.rb`](https://github.com/discourse/discourse/blob/b46b6e72d1906ca31e29855bda71f3498c8e203f/lib/discourse.rb#L215-L222). A full list of possible `AuthProvider` options can be found [here](https://github.com/discourse/discourse/blob/b46b6e72d1906ca31e29855bda71f3498c8e203f/lib/auth/auth_provider.rb#L8-L12). Text content **can** be defined using these options, but it is better to provide localisable strings in `client.en.yml` following the standard keys. For example:

> <https://github.com/discourse/discourse-openid-connect/blob/88fdf7b5ab624aba7c207e403665e0393334794a/config/locales/client.en.yml#L1-L7>

> **Additional ManagedAuthenticator notes by @fantasticfears**
>
> ## `ManagedAuthenticator` in details
> 
> You might need to work on something special for authentication. And you would like to know more about `ManagedAuthenticator`. Basically, it has several operations, options, and controls how the data will be used.
> 
> Discourse manages user information with two controllers. `Users::OmniauthCallbacksController` manages the payload once OAuth2 authentication is done. `after_authenticate` is called here. `can_connect_existing_user?` is also used here.  
> There are some private methods you can read to understand how different data fields work.
> 
> ```rb
> if authenticator.can_connect_existing_user? && current_user
> @auth_result = authenticator.after_authenticate(auth, existing_account: current_user)
> else
> @auth_result = authenticator.after_authenticate(auth)
> end
> 
> ```
> 
> `UsersController` has `revoke_account` which uses `can_revoke?` and `revoke`. But for the `revoke` method to work remotely, you need to build your own implementation.
> 
> `UserAuthenticator` is a service class helping authenticate (verifying email confirmation or OAuth2 path) users. `after_create_account` is called here.
> 
> The core logic remains at `after_authenticate` with `Auth::Result` data class. We follow data structure here. `extra_data` will be passed to `after_create_account` for creating related records.
> 
> ```rb
> result.extra_data = {
> provider: auth_token[:provider],
> uid: auth_token[:uid],
> info: auth_token[:info],
> extra: auth_token[:extra],
> credentials: auth_token[:credentials]
> }
> 
> ```
> 
> It will try to match and connects to an existing account.
> 
> You might wonder why automatic account creation is possible but there is no `User.create`. This is done in `UsersController#create`.
> 
> ```rb
> authentication = UserAuthenticator.new(user, session)
> 
> ```
> 
> The user is a fresh instance will be populated by session data which is prepared by the auth provider. Trust me, it’s just magic.

* * *

### Migration to the new system

To provide a seamless switch to the new system, data should be migrated from the old storage location. For core authentication providers, this may be dedicated tables. For plugins, this may be `plugin_store_rows`, or `oauth2_user_infos`. The minimum data required in a `user_associated_accounts` row is `provider_name`, `provider_uid` and `user_id`. For an example migration see:

> <https://github.com/discourse/discourse/blob/main/db/migrate/20181207141900_migrate_twitter_user_info.rb>

Once the `ManagedAuthenticator` system has been released to the stable branch with v2.2.0, we will begin migrating official authentication **plugins**. At this point, a `plugin_store_row` migration example will be added here.

* * *

This document is version controlled - suggest changes [on github](https://github.com/discourse/discourse/blob/main/docs/developer-guides/docs/03-code-internals/17-authentication-method.md).

---

<div class="post-metadata">

### Author: ![fzngagan](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/fzngagan/32/259349_2.png) [@fzngagan](https://meta.discourse.org/u/fzngagan)
#### Post date: [February 7, 2022, 10:44am UTC](https://meta.discourse.org/t/adding-a-new-managed-authentication-method-to-discourse/106695/6 "2022-02-07T10:44:10Z")

</div>

@david all the work done here is super cool. Appreciate a ton. I also got a chance to play with [GitHub - discourse/discourse-development-auth: A discourse plugin which adds a fake authentication provider. For development purposes only. · GitHub](https://github.com/discourse/discourse-development-auth) which is super handy.

Just one caveat, the feature doesn’t play nicely(doesn’t bring up the signup popup) with ember cli on local. I was scratching my head while writing a plugin for adding and auth provider and suddenly it occured to me to use `NO_EMBER_CLI=1` and it all started working.

---

<div class="post-metadata">

### Author: ![thoka](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/thoka/32/115652_2.png) [@thoka](https://meta.discourse.org/u/thoka)
#### Post date: [November 29, 2023, 7:48am UTC](https://meta.discourse.org/t/adding-a-new-managed-authentication-method-to-discourse/106695/7 "2023-11-29T07:48:14Z")

</div>

I would like to learn, if implementing an authenticator would be the right path towards

> [@Simple login by email via deep links containing a username](https://meta.discourse.org/t/simple-login-by-email-via-deep-links-containing-a-username/286910):
>
> I would like to lower the hurdle to login in our school forum by the following procedure: Links sent by mail to the (mostly non-public) forum should contain the username of the addressed user (like forum.my.tld/t/123#user=toka) If a not logged-in user opens this link, a page offers to send a login link by clicking a button. If clicked, a login with a login token should be sent to the user, which then allows them to open the target page without further interaction for a specific time. Ot…

Do I understand right, that all registered authenticators are called early in the app, so I could test therein, if a username and some hint for email authentication is included in the URL and render a “send me a login link” form as response?
