Añadir un nuevo método de autenticación 'managed' a Discourse

Continuando desde Future Social Authentication Improvements…

Actualmente estamos en proceso de mover toda la información de las ‘cuentas asociadas’ a una única tabla de base de datos. Esto ayudará a reducir significativamente la lógica duplicada y permitirá un desarrollo más rápido en el futuro. Por ejemplo, migrar nuestra lógica central de Twitter al nuevo sistema redujo el número de líneas de código de 136 a solo 24 :tada:.

Esta publicación no está diseñada para ser un manual de instrucciones paso a paso para agregar un nuevo proveedor de autenticación, pero buscará proporcionar una visión general, señalando el código fuente relevante donde sea necesario.

Implementando un autenticador

Cada autenticador debe implementar una subclase de Auth::Authenticator. Para usar la nueva lógica compartida, el autenticador puede en su lugar extender Auth::ManagedAuthenticator. Un ejemplo de una implementación básica se puede encontrar en el autenticador de Facebook del núcleo:

name y register_middleware deben ser sobrescritos por las clases que lo implementan, junto con enable_setting — la configuración booleana del sitio que un administrador utiliza para activar el proveedor.

Un autenticador debería también declarar required_settings: las configuraciones del sitio que deben tener un valor antes de que una autenticación pueda tener éxito. La clase base las utiliza para configured?, y enabled? es enable_setting && configured?, por lo que un proveedor con credenciales faltantes nunca se anuncia en la página de inicio de sesión y su ruta /auth/<name> permanece cerrada; de lo contrario, hacer clic en el botón deja al usuario varado en la página de error del propio proveedor sin forma de volver. Declarar required_settings también permite que AuthProviderCredentialsValidator se niegue a habilitar el proveedor en primer lugar; conéctalo con validator: "AuthProviderCredentialsValidator" en la configuración de habilitación.

def enable_setting
  :enable_google_oauth2_logins
end

def required_settings
  %i[google_oauth2_client_id google_oauth2_client_secret]
end

Un autenticador que sobrescribe enabled? directamente opta por excluirse de ambas verificaciones.

:information_source: Aclaración: para la compatibilidad multisitio, es importante que cualquier información específica del sitio se proporcione a omniauth en una lambda setup, en lugar de ser fija en el momento de la definición. Consulte todos los autenticadores del núcleo para ejemplos de esto.

Toda la lógica para vincular cuentas externas con cuentas de Discourse es manejada por Auth::ManagedAuthenticator. Esto depende de que el proveedor de omniauth devuelva datos en el formato definido en su documentación. Si se requiere alguna manipulación de estos datos, los Autenticadores pueden sobrescribir el método after_authenticate y manipular el auth_token según sea necesario. Por ejemplo, el autenticador de Twitter del núcleo elimina toda la información extra del token:

Los datos se almacenan en la tabla de base de datos user_associated_accounts. provider_uid, info, credentials y extra se toman directamente de los datos devueltos por omniauth.

Las imágenes del proveedor se descargan desde info.image durante la autenticación y se retienen en el avatar_upload_id de cada cuenta. Permanecen separadas de la imagen subida por el usuario y de los preajustes seleccionables. El selector de avatares ofrece imágenes en caché de proveedores habilitados bajo los permisos de avatar existentes.

UserAvatar posee las importaciones de avatares, la selección, las actualizaciones y la limpieza. La autenticación programa la recuperación del proveedor a través de UserAvatar.retrieve_for_associated_account; los trabajos de descarga y los importadores usan UserAvatar.import_url_for_user, pasando associated_account_id para las imágenes del proveedor. Los métodos de conveniencia del usuario y las callbacks del ciclo de vida de la cuenta/carga delegan los cambios de avatar a UserAvatar; los permisos permanecen en Guardian.

user_avatars.selected_user_associated_account_id registra un proveedor seleccionado explícitamente. Las descargas posteriores actualizan el avatar mostrado solo mientras ese proveedor permanezca seleccionado. Las nuevas cuentas inicialmente seleccionan su proveedor cuando no se asignó un avatar; auth_overrides_avatar continúa aplicando la selección del proveedor. Los avatares existentes conservan su apariencia y no se les asigna un proveedor automáticamente. Las cuentas vinculadas existentes pueblan sus opciones de proveedor en su próximo inicio de sesión.

Elegir otro avatar borra la selección del proveedor. Desconectar una cuenta preserva su imagen actualmente mostrada como una instantánea local y preserva cualquier imagen subida separada. Las descargas fallidas retienen la imagen anterior. Las descargas en cola se descartan si la cuenta fue desconectada, movida a otro usuario, o ahora proporciona una URL de imagen diferente.

La selección de Gravatar aún utiliza el coincidir de ID de carga. Por lo tanto, una imagen personalizada o preajustada idéntica al Gravatar en caché puede seguir las actualizaciones posteriores de Gravatar.

Una vez que se ha definido una clase Authenticator, necesita ser registrada. Esto debe ocurrir temprano en el ciclo de vida de la aplicación, y no puede ocurrir dentro del método after_initialize de un plugin. El registro mínimo puede contener simplemente una referencia al autenticador. En un plugin, el registro se puede hacer usando la función auth_provider. Por ejemplo:

auth_provider authenticator: OpenIDConnectAuthenticator.new()

En el núcleo, el registro tiene lugar en discourse.rb. Una lista completa de las opciones posibles de AuthProvider se puede encontrar aquí. El contenido de texto puede ser definido usando estas opciones, pero es mejor proporcionar cadenas localizables en client.en.yml siguiendo las claves estándar. Por ejemplo:

Notas adicionales de ManagedAuthenticator por @fantasticfears

ManagedAuthenticator en detalle

Es posible que necesites trabajar en algo especial para la autenticación. Y querrás saber más sobre ManagedAuthenticator. Básicamente, tiene varias operaciones, opciones y controla cómo se usarán los datos.

Discourse gestiona la información del usuario con dos controladores. Users::OmniauthCallbacksController gestiona la carga útil una vez que la autenticación OAuth2 está hecha. after_authenticate se llama aquí. can_connect_existing_user? también se usa aquí.
Hay algunos métodos privados que puedes leer para entender cómo funcionan los diferentes campos de datos.

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 tiene revoke_account que usa can_revoke? y revoke. Pero para que el método revoke funcione de forma remota, necesitas construir tu propia implementación.

UserAuthenticator es una clase de servicio que ayuda a autenticar (verificando la confirmación de correo electrónico o la ruta OAuth2) a los usuarios. after_create_account se llama aquí.

La lógica central permanece en after_authenticate con la clase de datos Auth::Result. Seguimos la estructura de datos aquí. extra_data se pasará a after_create_account para crear registros relacionados.

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

Intentará coincidir y conectarse con una cuenta existente.

Es posible que te preguntes por qué es posible la creación automática de cuentas pero no hay User.create. Esto se hace en UsersController#create.

authentication = UserAuthenticator.new(user, session)

El usuario es una instancia nueva que será poblada con datos de sesión preparados por el proveedor de autenticación. Confía en mí, es solo magia.


Migración al nuevo sistema

Para proporcionar un cambio sin problemas al nuevo sistema, los datos deben migrarse desde la ubicación de almacenamiento antigua. Para los proveedores de autenticación del núcleo, esto puede ser tablas dedicadas. Para los plugins, esto puede ser plugin_store_rows, o oauth2_user_infos. Los datos mínimos requeridos en una fila de user_associated_accounts son provider_name, provider_uid y user_id. Para un ejemplo de migración ver:

Una vez que el sistema ManagedAuthenticator haya sido liberado en la rama estable con v2.2.0, comenzaremos a migrar los plugins de autenticación oficiales. En este punto, se añadirá aquí un ejemplo de migración de plugin_store_row.


Este documento está controlado por versiones - sugiere cambios en github.

23 Me gusta

@david todo el trabajo realizado aquí es súper bueno. Lo aprecio mucho. También tuve la oportunidad de jugar con GitHub - discourse/discourse-development-auth: A discourse plugin which adds a fake authentication provider. For development purposes only., que es muy útil.

Solo una advertencia, la función no funciona bien (no muestra la ventana emergente de registro) con ember cli en local. Me rompí la cabeza mientras escribía un plugin para agregar un proveedor de autenticación y de repente se me ocurrió usar NO_EMBER_CLI=1 y todo empezó a funcionar.

7 Me gusta

Me gustaría saber si implementar un autenticador sería el camino correcto hacia

¿Entiendo correctamente que todos los autenticadores registrados se llaman al principio de la aplicación, por lo que podría probar allí si el nombre de usuario y alguna pista para la autenticación por correo electrónico están incluidos en la URL y mostrar un formulario de “enviarme un enlace de inicio de sesión” como respuesta?