Continuing from Future Social Authentication Improvements…
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 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. To use the new shared logic, the authenticator can instead extend Auth::ManagedAuthenticator. An example of a bare-bones implementation can be found in the core Facebook authenticator:
name
, enabled?
and register_middleware
must be overridden by implementing classes.
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. 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:
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.
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:
auth_provider authenticator: OpenIDConnectAuthenticator.new()
In core, registration takes place in discourse.rb
. A full list of possible AuthProvider
options can be found here. 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:
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.
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.
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
.
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:
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.
Last edited by @JammyDodger 2024-05-25T08:47:29Z
Check document
Perform check on document: