• técnico
  • contas
  • doações
  • lfg

O que mudou no sistema de login?

Este post é uma continuação do post anterior, porém entrando nos detalhes técnicos do que mudou no sistema de login único (SSO) do RPGist, o Accounts, e porque.

Anteriormente, ao clicar no botão de login do LFG, por exemplo, o usuário era redirecionado para o Accounts para iniciar uma sessão, caso não tivesse uma, e então era redirecionado de volta para o LFG com um token de acesso na URL que o LFG armazenava e usava para iniciar a própria sessão e para requisitar informações de usuário, que estão armazenadas no Accounts.

Este tipo de fluxo, no entanto, tem 2 problemas:

  1. como cada app é responsável por iniciar e destruir a própria sessão, as apps não compartilham login entre si: ao logar no LFG, você não estará logado automaticamente no Donate e vice-versa
  2. todas as aplicações são obrigadas a ter uma lógica repetida de armazenamento de token de sessão, que se mudasse por alguma razão, precisaria ser alterado individualmente em cada app

A solução que implementei para estes problemas não é das mais bonitas, mas foi a melhor que encontrei até o momento.

Agora, a responsabilidade de iniciar as sessões em todas as apps do RPGist não pertence mais às apps individualmente, mas ao próprio Accounts: ao logar no Accounts, ele mesmo escreve o cookie de sessão em cada uma das apps integradas antes de redirecionar o usuário de volta para a app que pediu o login. Da mesma forma, quando o usuário clica em um link para se deslogar de qualquer app, ele é redirecionado para o Accounts, que apaga o cookie de sessão de todas as apps e depois termina a própria sessão.

A dificuldade enfrentada é que cada app existe no seu próprio domínio e, por questões de segurança, os navegadores não permitem que sites em domínios diferentes possam ler ou escrever cookies de sites em outros domínios.

Como todas as apps do RPGist estão em subdomínios do rpgist.net, até seria possível escrever um cookie compartilhado, mas a solução não funcionaria em ambiente de desenvolvimento, onde as apps rodam em http://localhost, cada uma na própria porta.

Como o Accounts consegue escrever o token de acesso nas outras apps então?

Da mesma forma que antes, ao clicar no botão de login de uma app, como o LFG por exemplo, o usuário é redirecionado para o Accounts, onde deve iniciar uma sessão, se não tiver uma. Agora, porém, ao invés de ser imediatamente redirecionado de volta, o usuário é direcionado a uma página de carregamento de login do próprio Accounts em que ele carrega um <iframe /> invisível para cada app integrada, dentro dos quais carrega suas respectivas páginas responsáveis por iniciar a sessão na app. 

Durante o logoff acontece a mesma coisa, o usuário é redirecionado para o accounts, que carrega as páginas responsáveis por destruir a sessão de cada app, e então termina a própria sessão.

Isto resolve o problema 1. Ao logar em uma app, está logado em todas. Ao deslogar de uma app, está deslogado de todas.

Para resolver o problema 2, a solução foi ainda mais gambiarra criativa.

A página responsável por iniciar e destruir sessões que existe em cada app, que é carregada nos <iframe /> mencionados acima, é um HTML estático armazenado na pasta pública de cada app. Uma cópia idêntica para cada uma. Se a lógica de iniciar e destruir sessão ficar no próprio HTML, entretanto, o problema persiste: se alguma coisa mudar na lógica de gerenciamento de sessão, precisarei copiar e colar o HTML atuallizado em cada uma das apps e fazer deploy de todas elas. Nada DRY.

A responsabilidade deste HTML, portanto, fica como apenas carregar um script armazenado no próprio Accounts que, ele sim, contém a lógica de criação e destruição de sessão. As lógicas, portanto, estão centralizadas em um só lugar, que é no serviço que tem a responsabildiade de gerenciar sessões.

Para facilitar ainda mais a manutenção, o carregamento deste script é feito dinamicamente, da mesma forma que fazem o Google Analytics e o SDK do Facebook, por exemplo, e a partir de uma URL que ele recebe por query params. Desta forma, o HTML sequer precisa armazenar o endereço do script no Accounts, que, por sinal, muda a cada vez que o script é atualizado devido aos sufixos que o Sprockets coloca nos nomes dos arquivos que ele precompila.

Finalmente, para não deixar o arquivo vulnerável a receber e executar scripts maliciosos, o script só é carregado se a URL recebida estiver abaixo do domínio do RPGist ou localhost.

Resumindo, o fluxo fica o seguinte:

  1. o usuário clica no link de login de qualquer app e é redirecionado para o Accounts passando o ID da app e uma URL de redirecionamento como query params
  2. o Accounts valida o ID da app e a URL de redirecionamento recebidos e, caso não estejam registradas em um whitelist, redireciona o usuário de volta, sem realizar o login
  3. se o ID e URL forem válidos, o usuário é apresentado à página de login e cadastro do Accounts
  4. ao se cadastrar e/ou logar, o usuário é redirecionado para uma página do Accounts que carrega os <iframe /> das apps integradas e mostra um ícone animado de carregamento enquanto os logins são realizados
  5. no src de cada <iframe />, são passados como query params o nome do token de acesso (para ser usado como chave na hora de escrever o cookie), o próprio token de acesso e a URL do script do Accounts responsável por escrever o token de acesso nos cookies da app
  6. o HTML carregado dentro do <iframe /> valida se a URL recebida pertence ao domínio do RPGist e carrega o scritp dinamicamente
  7. o script carregado lê o nome e valor do token de acesso e escreve o token nos cookies da app
  8. o Accounts observa o evento de carregamento de cada <iframe /> e quanto todos terminam de carregar, redireciona o usuário de volta para a URL de redirecionamento que recebeu no passo 1

Voilà! Single Sign On!

PS: se alguém tiver sugestões de como melhorar isso ainda mais, fiquem a vontade para deixar comentários abaixo ou chamar no chat da página no Facebook, ou pode mandar um e-mail apra admin@rpgist.net também. Agradeço!