Ajout d'une nouvelle méthode d'authentification « gérée » à Discourse

Suite de Future Social Authentication Improvements…

Nous sommes actuellement en train de regrouper toutes les informations relatives aux « comptes associés » dans une seule table de base de données. Cela permettra de réduire considérablement la logique dupliquée et de faciliter le développement à l’avenir. Par exemple, la migration de notre logique Twitter principale vers le nouveau système a réduit le nombre de lignes de code de 136 à seulement 24 :tada:.

Ce post n’est pas conçu pour être un manuel d’instructions étape par étape pour ajouter un nouveau fournisseur d’authentification, mais il vise à fournir un aperçu général, en renvoyant vers le code source pertinent lorsque nécessaire.

Implémentation d’un authentificateur

Chaque authentificateur doit implémenter une sous-classe de Auth::Authenticator. Pour utiliser la nouvelle logique partagée, l’authentificateur peut au lieu de cela étendre Auth::ManagedAuthenticator. Un exemple d’implémentation minimale peut être trouvé dans l’authentificateur Facebook du cœur :

name et register_middleware doivent être redéfinis par les classes d’implémentation, de même que enable_setting — le paramètre de site booléen qu’un administrateur utilise pour activer le fournisseur.

Un authentificateur devrait également déclarer required_settings : les paramètres de site qui doivent avoir une valeur avant qu’une authentification ne puisse réussir. La classe de base les utilise pour configured?, et enabled? est enable_setting && configured?, de sorte qu’un fournisseur avec des identifiants manquants n’est jamais annoncé sur la page de connexion et que sa route /auth/<name> reste fermée — sinon, cliquer sur le bouton laisserait l’utilisateur bloqué sur la page d’erreur du fournisseur sans moyen de retour. Déclarer required_settings permet également à AuthProviderCredentialsValidator de refuser d’activer le fournisseur en premier lieu ; connectez-le avec validator: "AuthProviderCredentialsValidator" sur le paramètre d’activation.

def enable_setting
  :enable_google_oauth2_logins
end

def required_settings
  %i[google_oauth2_client_id google_oauth2_client_secret]
end

Un authentificateur qui redéfinit directement enabled? opte pour la sortie de ces deux contrôles.

:information_source: Note : pour la compatibilité multisite, il est important que toute information spécifique au site soit fournie à omniauth dans une lambda setup, plutôt que d’être fixée au moment de la définition. Consultez tous les authentificateurs du cœur pour des exemples de cela.

Toute la logique pour lier les comptes externes aux comptes Discourse est gérée par Auth::ManagedAuthenticator. Cela repose sur le fournisseur omniauth qui renvoie des données au format défini dans leur documentation. Si une manipulation de ces données est requise, les Authentificateurs peuvent redéfinir la méthode after_authenticate et manipuler le auth_token comme nécessaire. Par exemple, l’authentificateur Twitter du cœur supprime toutes les informations extra du jeton :

Les données sont stockées dans la table de base de données user_associated_accounts. provider_uid, info, credentials et extra sont tous pris directement des données renvoyées par omniauth.

Les images des fournisseurs sont téléchargées depuis info.image lors de l’authentification et conservées dans avatar_upload_id de chaque compte. Elles restent distinctes de l’image téléversée par l’utilisateur et des préréglages sélectionnables. Le sélecteur d’avatars propose des images en cache des fournisseurs activés sous les permissions d’avatar existantes.

UserAvatar gère les importations, la sélection, les rafraîchissements et le nettoyage des avatars. L’authentification planifie la récupération du fournisseur via UserAvatar.retrieve_for_associated_account ; les tâches de téléchargement et les importateurs utilisent UserAvatar.import_url_for_user, en passant associated_account_id pour les images des fournisseurs. Les méthodes de commodité de l’utilisateur et les callbacks de cycle de vie des comptes/téléversements délèguent les changements d’avatar à UserAvatar ; les permissions restent dans Guardian.

user_avatars.selected_user_associated_account_id enregistre un fournisseur explicitement sélectionné. Les téléchargements ultérieurs mettent à jour l’avatar affiché uniquement tant que ce fournisseur reste sélectionné. Les nouveaux comptes sélectionnent initialement leur fournisseur lorsqu’aucun avatar n’a été attribué ; auth_overrides_avatar continue d’appliquer la sélection du fournisseur. Les avatars existants conservent leur apparence et ne sont pas automatiquement attribués à un fournisseur. Les comptes liés existants peuplent leurs choix de fournisseur lors de leur prochaine connexion.

Choisir un autre avatar efface la sélection du fournisseur. La déconnexion d’un compte conserve son image actuellement affichée comme instantané local et conserve toute image téléversée distincte. Les téléchargements échoués conservent l’image précédente. Les téléchargements en file d’attente sont abandonnés si le compte a été déconnecté, déplacé vers un autre utilisateur, ou fournit désormais une URL d’image différente.

La sélection Gravatar utilise toujours l’appariement d’ID de téléversement. Une image personnalisée ou préréglée identique au Gravatar en cache peut donc suivre les mises à jour ultérieures du Gravatar.

Une fois qu’une classe Authenticator a été définie, elle doit être enregistrée. Cela doit se faire tôt dans le cycle de vie de l’application, et ne peut pas se produire dans la méthode after_initialize d’un plugin. L’enregistrement minimal peut simplement contenir une référence à l’authentificateur. Dans un plugin, l’enregistrement peut être effectué en utilisant la fonction auth_provider. Par exemple :

auth_provider authenticator: OpenIDConnectAuthenticator.new()

Dans le cœur, l’enregistrement a lieu dans discourse.rb. Une liste complète des options possibles de AuthProvider peut être trouvée ici. Le contenu texte peut être défini en utilisant ces options, mais il est préférable de fournir des chaînes localisables dans client.en.yml en suivant les clés standard. Par exemple :

Notes supplémentaires sur ManagedAuthenticator par @fantasticfears

ManagedAuthenticator en détail

Vous pourriez avoir besoin de travailler sur quelque chose de spécial pour l’authentification. Et vous aimeriez en savoir plus sur ManagedAuthenticator. Fondamentalement, il a plusieurs opérations, options et contrôle la façon dont les données seront utilisées.

Discourse gère les informations utilisateur avec deux contrôleurs. Users::OmniauthCallbacksController gère la charge utile une fois l’authentification OAuth2 terminée. after_authenticate est appelé ici. can_connect_existing_user? est également utilisé ici.
Il y a certaines méthodes privées que vous pouvez lire pour comprendre comment fonctionnent les différents champs de données.

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 a revoke_account qui utilise can_revoke? et revoke. Mais pour que la méthode revoke fonctionne à distance, vous devez construire votre propre implémentation.

UserAuthenticator est une classe de service qui aide à authentifier (vérifier la confirmation par e-mail ou le chemin OAuth2) les utilisateurs. after_create_account est appelé ici.

La logique principale reste dans after_authenticate avec la classe de données Auth::Result. Nous suivons la structure de données ici. extra_data sera passé à after_create_account pour créer les enregistrements associés.

result.extra_data = {
  provider: auth_token[:provider],
  uid: auth_token[:uid],
  info: auth_token[:info],
  extra: auth_token[:extra],
  credentials: auth_token[:credentials]
}

Il essaiera de correspondre et de se connecter à un compte existant.

Vous vous demandez peut-être pourquoi la création automatique de compte est possible mais qu’il n’y a pas de User.create. Cela est fait dans UsersController#create.

authentication = UserAuthenticator.new(user, session)

L’utilisateur est une nouvelle instance qui sera peuplée par les données de session préparées par le fournisseur d’authentification. Faites-moi confiance, c’est juste de la magie.


Migration vers le nouveau système

Pour fournir un basculement sans heurts vers le nouveau système, les données doivent être migrées depuis l’emplacement de stockage ancien. Pour les fournisseurs d’authentification du cœur, cela peut être des tables dédiées. Pour les plugins, cela peut être plugin_store_rows, ou oauth2_user_infos. Les données minimales requises dans une ligne user_associated_accounts sont provider_name, provider_uid et user_id. Pour un exemple de migration, voir :

Une fois que le système ManagedAuthenticator aura été publié sur la branche stable avec la v2.2.0, nous commencerons à migrer les plugins d’authentification officiels. À ce stade, un exemple de migration de plugin_store_row sera ajouté ici.


Ce document est sous contrôle de version - suggérez des modifications sur github.

23 « J'aime »

@david tout le travail effectué ici est super cool. J’apprécie énormément. J’ai également eu l’occasion de jouer avec GitHub - discourse/discourse-development-auth: A discourse plugin which adds a fake authentication provider. For development purposes only. qui est très pratique.

Juste une mise en garde, la fonctionnalité ne fonctionne pas bien (n’affiche pas la fenêtre contextuelle d’inscription) avec ember cli en local. Je me suis creusé la tête en écrivant un plugin pour ajouter un fournisseur d’authentification et soudain, il m’est venu à l’idée d’utiliser NO_EMBER_CLI=1 et tout a commencé à fonctionner.

7 « J'aime »

J’aimerais savoir si la mise en œuvre d’un authentificateur serait la bonne approche pour

Ai-je bien compris que tous les authentificateurs enregistrés sont appelés tôt dans l’application, de sorte que je pourrais y tester si un nom d’utilisateur et un indice pour l’authentification par e-mail sont inclus dans l’URL et rendre un formulaire « envoyez-moi un lien de connexion » en réponse ?