Adicionando um novo método de autenticação 'gerenciado' ao Discourse

Continuando de Future Social Authentication Improvements…

Estamos agora no processo de mover todas as informações de ‘contas associadas’ para uma única tabela de banco de dados. Isso ajudará a reduzir significativamente a lógica duplicada e permitirá um desenvolvimento mais rápido no futuro. Por exemplo, migrar nossa lógica central do twitter para o novo sistema reduziu o número de linhas de código de 136 para apenas 24 :tada:.

Este post não foi projetado para ser um manual de instruções passo a passo para adicionar um novo provedor de autenticação, mas visa fornecer uma visão geral, apontando para o código-fonte relevante onde necessário.

Implementando um autenticador

Cada autenticador deve implementar uma subclasse de Auth::Authenticator. Para usar a nova lógica compartilhada, o autenticador pode em vez disso estender Auth::ManagedAuthenticator. Um exemplo de uma implementação básica pode ser encontrado no autenticador core do Facebook:

name e register_middleware devem ser sobrescritos pelas classes implementadoras, juntamente com enable_setting — a configuração booleana do site que um administrador usa para ativar o provedor.

Um autenticador deve também declarar required_settings: as configurações do site que devem ter um valor antes que uma autenticação possa ter sucesso. A classe base as usa para configured?, e enabled? é enable_setting && configured?, portanto, um provedor com credenciais ausentes nunca é anunciado na página de login e sua rota /auth/<name> permanece fechada — caso contrário, clicar no botão deixaria o usuário preso na própria página de erro do provedor, sem caminho de volta. Declarar required_settings também permite que o AuthProviderCredentialsValidator recuse ativar o provedor em primeiro lugar; conecte-o com validator: "AuthProviderCredentialsValidator" na configuração de ativação.

def enable_setting
  :enable_google_oauth2_logins
end

def required_settings
  %i[google_oauth2_client_id google_oauth2_client_secret]
end

Um autenticador que sobrescreve enabled? diretamente opta por sair de ambas as verificações.

:information_source: Nota: para compatibilidade multi-site, é importante que qualquer informação específica do site seja fornecida ao omniauth em um lambda setup, em vez de ser fixada no momento da definição. Veja todos os autenticadores core para exemplos disso.

Toda a lógica para vincular contas externas a contas do Discourse é tratada pelo Auth::ManagedAuthenticator. Isso depende do provedor omniauth retornar dados no formato definido em sua documentação. Se qualquer manipulação desses dados for necessária, os Autenticadores podem sobrescrever o método after_authenticate e manipular o auth_token conforme necessário. Por exemplo, o autenticador core do Twitter remove todas as informações extra do token:

Os dados são armazenados na tabela de banco de dados user_associated_accounts. provider_uid, info, credentials e extra são todos obtidos diretamente dos dados retornados pelo omniauth.

As imagens dos provedores são baixadas de info.image na autenticação e retidas no avatar_upload_id de cada conta. Elas permanecem separadas da foto enviada pelo usuário e dos presets selecionáveis. O seletor de avatares oferece imagens em cache dos provedores habilitados sob as permissões de avatar existentes.

UserAvatar possui a importação, seleção, atualização e limpeza de avatares. A autenticação agenda a recuperação do provedor através de UserAvatar.retrieve_for_associated_account; os trabalhos de download e importadores usam UserAvatar.import_url_for_user, passando associated_account_id para imagens do provedor. Os métodos de conveniência do usuário e os callbacks de ciclo de vida da conta/envio delegam as mudanças de avatar para UserAvatar; as permissões permanecem no Guardian.

user_avatars.selected_user_associated_account_id registra um provedor selecionado explicitamente. Downloads subsequentes atualizam o avatar exibido apenas enquanto esse provedor permanecer selecionado. Novas contas inicialmente selecionam seu provedor quando nenhum avatar foi atribuído; auth_overrides_avatar continua a impor a seleção do provedor. Avatares existentes mantêm sua aparência e não recebem um provedor automaticamente. Contas vinculadas existentes preenchem suas escolhas de provedor no próximo login.

Escolher outro avatar limpa a seleção do provedor. Desconectar uma conta preserva sua imagem atualmente exibida como um snapshot local e preserva qualquer foto enviada separadamente. Downloads falhos retêm a imagem anterior. Downloads na fila são descartados se a conta foi desconectada, movida para outro usuário ou agora fornece uma URL de imagem diferente.

A seleção do Gravatar ainda usa o correspondência de ID de upload. Uma foto personalizada ou preset idêntica ao Gravatar em cache pode, portanto, seguir as atualizações subsequentes do Gravatar.

Uma vez que uma classe Authenticator tenha sido definida, ela precisa ser registrada. Isso deve acontecer cedo no ciclo de vida da aplicação e não pode acontecer dentro do método after_initialize de um plugin. O registro mínimo pode simplesmente conter uma referência ao autenticador. Em um plugin, o registro pode ser feito usando a função auth_provider. Por exemplo:

auth_provider authenticator: OpenIDConnectAuthenticator.new()

No core, o registro ocorre em discourse.rb. Uma lista completa de opções possíveis de AuthProvider pode ser encontrada aqui. O conteúdo de texto pode ser definido usando essas opções, mas é melhor fornecer strings localizáveis em client.en.yml seguindo as chaves padrão. Por exemplo:

Notas adicionais sobre ManagedAuthenticator por @fantasticfears

ManagedAuthenticator em detalhes

Você pode precisar trabalhar em algo especial para autenticação. E você gostaria de saber mais sobre ManagedAuthenticator. Basicamente, ele tem várias operações, opções e controla como os dados serão usados.

O Discourse gerencia as informações do usuário com dois controladores. Users::OmniauthCallbacksController gerencia o payload uma vez que a autenticação OAuth2 é concluída. after_authenticate é chamado aqui. can_connect_existing_user? também é usado aqui.
Existem alguns métodos privados que você pode ler para entender como diferentes campos de dados funcionam.

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 tem revoke_account que usa can_revoke? e revoke. Mas para que o método revoke funcione remotamente, você precisa construir sua própria implementação.

UserAuthenticator é uma classe de serviço que ajuda a autenticar (verificando confirmação de e-mail ou caminho OAuth2) usuários. after_create_account é chamado aqui.

A lógica central permanece em after_authenticate com a classe de dados Auth::Result. Seguimos a estrutura de dados aqui. extra_data será passado para after_create_account para criar 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]
}

Ele tentará corresponder e conectar a uma conta existente.

Você pode se perguntar por que a criação automática de conta é possível, mas não há User.create. Isso é feito em UsersController#create.

authentication = UserAuthenticator.new(user, session)

O usuário é uma instância nova que será preenchida com dados da sessão que são preparados pelo provedor de autenticação. Confie em mim, é apenas mágica.


Migração para o novo sistema

Para fornecer uma troca sem emendas para o novo sistema, os dados devem ser migrados do local de armazenamento antigo. Para provedores de autenticação core, isso pode ser tabelas dedicadas. Para plugins, isso pode ser plugin_store_rows, ou oauth2_user_infos. Os dados mínimos necessários em uma linha user_associated_accounts são provider_name, provider_uid e user_id. Para um exemplo de migração veja:

Uma vez que o sistema ManagedAuthenticator tenha sido lançado para o branch estável com a v2.2.0, começaremos a migrar os plugins oficiais de autenticação. Neste ponto, um exemplo de migração de plugin_store_row será adicionado aqui.


Este documento é controlado por versão - sugira alterações no github.

23 curtidas

@david todo o trabalho feito aqui é super legal. Agradeço muito. Eu também tive a chance de brincar com GitHub - discourse/discourse-development-auth: A discourse plugin which adds a fake authentication provider. For development purposes only., que é super útil.

Apenas uma ressalva, o recurso não funciona bem (não exibe o pop-up de inscrição) com o ember cli localmente. Eu estava quebrando a cabeça ao escrever um plugin para adicionar um provedor de autenticação e, de repente, me ocorreu usar NO_EMBER_CLI=1 e tudo começou a funcionar.

7 curtidas

Gostaria de saber se implementar um autenticador seria o caminho certo para

Eu entendi corretamente que todos os autenticadores registrados são chamados no início do aplicativo, então eu poderia testar neles se um nome de usuário e alguma dica para autenticação por e-mail estão incluídos na URL e renderizar um formulário de “envie-me um link de login” como resposta?