Discourse に新しい「管理」認証方法を追加

Future Social Authentication Improvements… を継続します。

現在、すべての「関連アカウント」情報を単一のデータベーステーブルに移行する作業を進めています。これにより、重複したロジックを大幅に削減し、将来の開発を迅速化できます。例えば、コアのTwitterロジックを新しいシステムに移行したことで、コード行数は136行からわずか24行に削減されました :tada:。

この投稿は、新しい認証プロバイダーを追加するためのステップバイステップのマニュアルを目的としたものではありませんが、必要に応じて関連するソースコードを指し示しながら、概要を提供することを目的としています。

認証器の実装

各認証器は、Auth::Authenticator のサブクラスを実装する必要があります。新しい共有ロジックを使用するには、認証器は代わりに Auth::ManagedAuthenticator を拡張することができます。最小限の実装の例は、コアのFacebook認証器にあります:

name と register_middleware は、実装クラスによってオーバーライドされなければなりません。また、enable_setting も同様です。これは、管理者がプロバイダーをオンにするために使用するブール型のサイト設定です。

認証器は required_settings も宣言するべきです。これは、認証が成功する前に値を持たなければならないサイト設定です。ベースクラスはこれらを configured? に使用し、enabled? は enable_setting && configured? となるため、認証情報が欠落しているプロバイダーはログインページに表示されず、その /auth/<name> ルートは閉じたままになります。そうしないと、ボタンをクリックしたユーザーがプロバイダー独自のエラーページに閉じ込められ、戻り道がなくなります。required_settings を宣言することで、AuthProviderCredentialsValidator がプロバイダーの有効化自体を拒否するようになります。有効化設定に validator: "AuthProviderCredentialsValidator" を設定して接続してください。

def enable_setting
  :enable_google_oauth2_logins
end

def required_settings
  %i[google_oauth2_client_id google_oauth2_client_secret]
end

enabled? を直接オーバーライドする認証器は、これらの両方のゲートから除外されます。

:information_source: 補足: マルチサイト互換性のために、サイト固有の情報は定義時に固定するのではなく、setup ラムダを通じて omniauth に供給することが重要です。例については、すべてのコア認証器を参照してください。

外部アカウントを Discourse アカウントにリンクするすべてのロジックは Auth::ManagedAuthenticator によって処理されます。これは、omniauth プロバイダーがドキュメントで定義された形式でデータを返すことに依存しています。このデータの操作が必要な場合、認証器は after_authenticate メソッドをオーバーライドし、必要に応じて auth_token を操作できます。例えば、コアのTwitter認証器はトークンからすべての extra 情報を削除します:

データは user_associated_accounts データベーステーブルに保存されます。provider_uid、info、credentials、extra はすべて、omniauth が返したデータから直接取得されます。

プロバイダーの画像は認証時に info.image からダウンロードされ、各アカウントの avatar_upload_id に保持されます。これらはユーザーがアップロードした画像や選択可能なプリセットとは別に管理されます。アバターピッカーは、既存のアバター権限の下で、有効化されたプロバイダーからのキャッシュされた画像を提供します。

UserAvatar はアバターのインポート、選択、更新、クリーンアップを管理します。認証は UserAvatar.retrieve_for_associated_account を通じてプロバイダーからの取得をスケジュールします。ダウンロードジョブとインポーターは UserAvatar.import_url_for_user を使用し、プロバイダー画像の場合は associated_account_id を渡します。ユーザーの便利メソッドとアカウント/アップロードのライフサイクルコールバックは、アバターの変更を UserAvatar に委譲し、権限は Guardian に残ります。

user_avatars.selected_user_associated_account_id は、明示的に選択されたプロバイダーを記録します。そのプロバイダーが選択されたままの間、以降のダウンロードは表示されるアバターを更新します。新しいアカウントは、アバターが割り当てられていない場合に、最初にプロバイダーを選択します。auth_overrides_avatar は引き続きプロバイダーの選択を強制します。既存のアバターはその外観を維持し、自動的にプロバイダーが割り当てられることはありません。既存のリンク済みアカウントは、次のログイン時にプロバイダーの選択を埋めます。

別のアバターを選択すると、プロバイダーの選択がクリアされます。アカウントの切断は、現在表示されている画像をローカルスナップショットとして保持し、別途アップロードされた画像も保持します。ダウンロードに失敗した場合、以前の画像が保持されます。キューに登録されたダウンロードは、アカウントが切断された、別のユーザーに移動された、または異なる画像URLを供給するようになった場合は破棄されます。

Gravatar の選択は引き続きアップロードIDのマッチングを使用します。そのため、キャッシュされた Gravatar と同一のカスタム画像やプリセット画像は、その後の Gravatar の更新に従う可能性があります。

Authenticator クラスが定義されると、登録が必要です。これはアプリケーションのライフサイクルの早い段階で行われなければならず、プラグインの after_initialize メソッド内で行うことはできません。最小限の登録には、認証器への参照を含むだけで十分です。プラグインでは、auth_provider 関数を使用して登録を行うことができます。例えば:

auth_provider authenticator: OpenIDConnectAuthenticator.new()

コアでは、登録は discourse.rb で行われます。AuthProvider のオプションの完全なリストはこちらで見つけることができます。これらのオプションを使用してテキストコンテンツを定義することができますが、標準的なキーに従って client.en.yml にローカライズ可能な文字列を提供する方が望ましいです。例えば:

@fantasticfears による ManagedAuthenticator に関する追加メモ

ManagedAuthenticator の詳細

認証のために特別な何かを作業する必要があるかもしれません。そして ManagedAuthenticator についてもっと知りたいと思うでしょう。基本的に、それはいくつかの操作やオプションを持ち、データがどのように使用されるかを制御します。

Discourse は2つのコントローラーでユーザー情報を管理します。Users::OmniauthCallbacksController は、OAuth2 認証が完了した後のペイロードを管理します。ここで after_authenticate が呼び出されます。can_connect_existing_user? もここで使用されます。
異なるデータフィールドがどのように動作するかを理解するために、読むことができるいくつかのプライベートメソッドがあります。

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 には revoke_account があり、これは can_revoke? と revoke を使用します。しかし、revoke メソッドをリモートで動作させるには、独自の実装を構築する必要があります。

UserAuthenticator は、ユーザーを認証(メール確認または OAuth2 パスの検証)するのに役立つサービスクラスです。ここで after_create_account が呼び出されます。

コアロジックは Auth::Result データクラスを持つ after_authenticate に残ります。ここではデータ構造に従います。extra_data は関連レコードの作成のために after_create_account に渡されます。

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

既存のアカウントとマッチングし、接続しようとします。

なぜ自動アカウント作成が可能なのに User.create がないのかと疑問に思うかもしれません。これは UsersController#create で行われます。

authentication = UserAuthenticator.new(user, session)

ユーザーは新しいインスタンスであり、認証プロバイダーによって準備されたセッションデータによって埋められます。信じられないかもしれませんが、ただのマジックです。


新しいシステムへの移行

新しいシステムへのシームレスな切り替えを提供するために、データは古い保存場所から移行する必要があります。コアの認証プロバイダーの場合、これは専用テーブルかもしれません。プラグインの場合、plugin_store_rows または oauth2_user_infos かもしれません。user_associated_accounts 行に必要な最小限のデータは provider_name、provider_uid、user_id です。移行の例については、以下を参照してください:

ManagedAuthenticator システムが v2.2.0 で安定版ブランチにリリースされると、公式の認証プラグインの移行を開始します。この時点で、plugin_store_row 移行の例がここに追加されます。


このドキュメントはバージョン管理されています - 変更の提案は github で行ってください。

「いいね!」 23

@david ここで行われたすべての作業は非常にクールです。大変感謝しています。また、非常に便利な GitHub - discourse/discourse-development-auth: A discourse plugin which adds a fake authentication provider. For development purposes only. も試す機会がありました。

ただ一つ注意点があります。この機能はローカルのEmber CLIとうまく連携しません(サインアップポップアップが表示されません)。プラグインを追加して認証プロバイダーを追加しようとしているときに頭を悩ませていましたが、NO_EMBER_CLI=1 を使用することを思いつき、すべてが機能し始めました。

「いいね!」 7

認証機能の実装が、

への正しいアプローチかどうかを知りたいです。

登録されているすべての認証機能がアプリの早い段階で呼び出されるため、URLにユーザー名とメール認証のヒントが含まれているかどうかをそこでテストし、「ログインリンクを送信」フォームを応答としてレンダリングできる、と理解して合っていますか?