Neue „managed“-Authentifizierungsmethode für Discourse

Fortsetzung von Future Social Authentication Improvements…

Wir befinden uns derzeit in der Phase, in der alle Informationen zu „verbundenen Konten“ in eine einzelne Datenbanktabelle überführt werden. Dies hilft dabei, doppelte Logik erheblich zu reduzieren und eine schnellere Entwicklung in Zukunft zu ermöglichen. Zum Beispiel hat die Migration unserer Kern-Twitter-Logik auf das neue System die Zeilenanzahl des Codes von 136 auf nur 24 reduziert :tada:.

Dieser Beitrag ist nicht als schrittweise Anleitung zum Hinzufügen eines neuen Authentifizierungsanbieters konzipiert, sondern soll einen Überblick geben und bei Bedarf auf den relevanten Quellcode verweisen.

Implementierung eines Authenticators

Jeder Authenticator muss eine Unterklasse von Auth::Authenticator implementieren. Um die neue gemeinsame Logik zu nutzen, kann der Authenticator stattdessen Auth::ManagedAuthenticator erweitern. Ein Beispiel für eine Minimal-Implementierung findet sich im Facebook-Authenticator im Kern:

name und register_middleware müssen von den implementierenden Klassen überschrieben werden, ebenso wie enable_setting – die boolesche Seiteneinstellung, mit der ein Administrator den Anbieter aktiviert.

Ein Authenticator sollte außerdem required_settings deklarieren: die Seiteneinstellungen, die einen Wert haben müssen, bevor eine Authentifizierung erfolgreich sein kann. Die Basisklasse verwendet diese für configured?, und enabled? ist enable_setting && configured?. Ein Anbieter mit fehlenden Anmeldedaten wird daher nie auf der Anmeldeseite beworben und seine /auth/<name>-Route bleibt geschlossen – andernfalls würde ein Klick auf den Button den Benutzer auf der eigenen Fehlerseite des Anbieters stranden, ohne Möglichkeit zurückzukehren. Die Deklaration von required_settings ermöglicht es auch AuthProviderCredentialsValidator, die Aktivierung des Anbieters von vornherein abzulehnen; verdrahten Sie dies mit validator: "AuthProviderCredentialsValidator" an der Einstellung zur Aktivierung.

def enable_setting
  :enable_google_oauth2_logins
end

def required_settings
  %i[google_oauth2_client_id google_oauth2_client_secret]
end

Ein Authenticator, der enabled? direkt überschreibt, optiert aus beiden Kontrollmechanismen aus.

:information_source: Randnotiz: Für die Kompatibilität mit Multi-Site-Setups ist es wichtig, dass site-spezifische Informationen Omniauth in einem setup-Lambda bereitgestellt werden, anstatt sie zur Zeit der Definition festzulegen. Siehe alle Kern-Authenticatoren für Beispiele hierfür.

Die gesamte Logik zur Verknüpfung externer Konten mit Discourse-Konten wird von Auth::ManagedAuthenticator behandelt. Dies setzt voraus, dass der Omniauth-Anbieter Daten im Format zurückgibt, das in deren Dokumentation definiert ist. Falls eine Manipulation dieser Daten erforderlich ist, können Authenticators die Methode after_authenticate überschreiben und den auth_token nach Bedarf manipulieren. Zum Beispiel entfernt der Kern-Twitter-Authenticator alle extra-Informationen aus dem Token:

Die Daten werden in der Datenbanktabelle user_associated_accounts gespeichert. provider_uid, info, credentials und extra werden direkt aus den von Omniauth zurückgegebenen Daten übernommen.

Anbieter-Bilder werden bei der Authentifizierung von info.image heruntergeladen und in avatar_upload_id jedes Kontos aufbewahrt. Sie bleiben getrennt vom hochgeladenen Bild des Benutzers und den wählbaren Voreinstellungen. Der Avatar-Auswahlbereich bietet zwischengespeicherte Bilder von aktivierten Anbietern unter den bestehenden Avatar-Berechtigungen an.

UserAvatar ist verantwortlich für Avatar-Importe, Auswahl, Aktualisierungen und Bereinigung. Die Authentifizierung plant die Abrufe des Anbieters über UserAvatar.retrieve_for_associated_account; Download-Jobs und Importer verwenden UserAvatar.import_url_for_user und übergeben associated_account_id für Anbieter-Bilder. Benutzer-Komfortmethoden und Lifecycle-Callbacks für Konten/Uploads delegieren Avatar-Änderungen an UserAvatar; die Berechtigungen verbleiben in Guardian.

user_avatars.selected_user_associated_account_id erfasst einen explizit gewählten Anbieter. Nachfolgende Downloads aktualisieren den angezeigten Avatar nur, solange dieser Anbieter ausgewählt bleibt. Neue Konten wählen zunächst ihren Anbieter aus, wenn kein Avatar zugewiesen war; auth_overrides_avatar erzwingt weiterhin die Anbieterauswahl. Bestehende Avatare behalten ihr Aussehen bei und erhalten nicht automatisch einen Anbieter zugewiesen. Bestehende verknüpfte Konten befüllen ihre Anbieterauswahl bei ihrem nächsten Login.

Die Auswahl eines anderen Avatars löscht die Anbieterauswahl. Das Trennen eines Kontos bewahrt das aktuell angezeigte Bild als lokale Momentaufnahme und erhält ein separat hochgeladenes Bild. Fehlgeschlagene Downloads behalten das vorherige Bild bei. Geplante Downloads werden verworfen, wenn das Konto getrennt wurde, zu einem anderen Benutzer verschoben wurde oder nun eine andere Bild-URL bereitstellt.

Die Gravatar-Auswahl verwendet weiterhin die Abgleichung der Upload-ID. Ein benutzerdefiniertes oder voreingestelltes Bild, das identisch mit dem zwischengespeicherten Gravatar ist, kann daher nachfolgenden Gravatar-Aktualisierungen folgen.

Sobald eine Authenticator-Klasse definiert wurde, muss sie registriert werden. Dies muss früh im Lebenszyklus der Anwendung erfolgen und kann nicht innerhalb der after_initialize-Methode eines Plugins geschehen. Die minimale Registrierung kann einfach einen Verweis auf den Authenticator enthalten. In einem Plugin kann die Registrierung mit der Funktion auth_provider erfolgen. Zum Beispiel:

auth_provider authenticator: OpenIDConnectAuthenticator.new()

Im Kern erfolgt die Registrierung in discourse.rb. Eine vollständige Liste der möglichen AuthProvider-Optionen findet sich hier. Textinhalte können mit diesen Optionen definiert werden, aber es ist besser, lokalisierte Zeichenfolgen in client.en.yml mit den Standard-Schlüsseln bereitzustellen. Zum Beispiel:

Zusätzliche Hinweise zu ManagedAuthenticator von @fantasticfears

ManagedAuthenticator im Detail

Vielleicht müssen Sie an etwas Besonderem für die Authentifizierung arbeiten. Und Sie möchten mehr über ManagedAuthenticator erfahren. Grundsätzlich hat es mehrere Operationen, Optionen und steuert, wie die Daten verwendet werden.

Discourse verwaltet Benutzerinformationen mit zwei Controllern. Users::OmniauthCallbacksController verwaltet die Nutzlast, sobald die OAuth2-Authentifizierung abgeschlossen ist. after_authenticate wird hier aufgerufen. can_connect_existing_user? wird hier ebenfalls verwendet.
Es gibt einige private Methoden, die Sie lesen können, um zu verstehen, wie die verschiedenen Datenfelder funktionieren.

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 hat revoke_account, das can_revoke? und revoke verwendet. Aber damit die revoke-Methode remote funktioniert, müssen Sie Ihre eigene Implementierung erstellen.

UserAuthenticator ist eine Service-Klasse, die bei der Authentifizierung (Verifizierung der E-Mail-Bestätigung oder des OAuth2-Pfads) von Benutzern hilft. after_create_account wird hier aufgerufen.

Die Kernlogik verbleibt bei after_authenticate mit der Auth::Result-Datenklasse. Wir folgen hier der Datenstruktur. extra_data wird an after_create_account übergeben, um verwandte Datensätze zu erstellen.

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

Es wird versuchen, mit einem bestehenden Konto zu matchen und sich zu verbinden.

Vielleicht fragen Sie sich, warum die automatische Kontenerstellung möglich ist, aber es kein User.create gibt. Dies geschieht in UsersController#create.

authentication = UserAuthenticator.new(user, session)

Der Benutzer ist eine neue Instanz, die durch Sitzungsdaten befüllt wird, die vom Authentifizierungsanbieter vorbereitet wurden. Vertrauen Sie mir, es ist nur Magie.


Migration auf das neue System

Um einen nahtlosen Wechsel auf das neue System zu ermöglichen, sollten die Daten vom alten Speicherort migriert werden. Für Kern-Authentifizierungsanbieter können dies dedizierte Tabellen sein. Für Plugins können dies plugin_store_rows oder oauth2_user_infos sein. Die minimalen Daten, die in einer user_associated_accounts-Zeile erforderlich sind, sind provider_name, provider_uid und user_id. Für ein Migrationsbeispiel siehe:

Sobald das ManagedAuthenticator-System mit v2.2.0 in den stabilen Zweig veröffentlicht wurde, werden wir mit der Migration offizieller Authentifizierungs-Plugins beginnen. Zu diesem Zeitpunkt wird hier ein Migrationsbeispiel für plugin_store_row hinzugefügt.


Dieses Dokument wird versioniert – schlagen Sie Änderungen auf GitHub vor.

23 „Gefällt mir“

@david Die hier geleistete Arbeit ist super cool. Ich schätze sie sehr. Ich hatte auch die Gelegenheit, mit GitHub - discourse/discourse-development-auth: A discourse plugin which adds a fake authentication provider. For development purposes only. zu spielen, was sehr praktisch ist.

Nur eine Einschränkung: Die Funktion funktioniert nicht gut (öffnet nicht das Anmelde-Popup) mit dem Ember CLI lokal. Ich habe mir den Kopf zerbrochen, als ich ein Plugin zum Hinzufügen eines Authentifizierungsanbieters schrieb, und plötzlich fiel mir ein, NO_EMBER_CLI=1 zu verwenden, und alles funktionierte.

7 „Gefällt mir“

Ich würde gerne erfahren, ob die Implementierung eines Authentifikators der richtige Weg ist, um

Verstehe ich richtig, dass alle registrierten Authentifikatoren früh in der App aufgerufen werden, sodass ich dort testen könnte, ob ein Benutzername und ein Hinweis zur E-Mail-Authentifizierung in der URL enthalten sind und als Antwort ein Formular “Senden Sie mir einen Login-Link” rendern könnte?