Single Sign-On (SSO) Server + Laravel 5

Deixei o monolítico de lado, parti pros microsserviços, o Single Sign-On (SSO) tá no meio de campo e o Laravel tá sendo Laravel =D

De antemão, eu quero dizer algo para você que chegou aqui através de buscas ou por ter encontrado esse link compartilhado em alguma rede social: não desista! Lidar com desafios tecnológicos é algo bastante desafiador. Andamos em muitos terrenos desconhecidos, afim de alcançar um objetivo. Alcançar um resultado é maravilhoso quando conseguimos, porém perturbador enquanto ainda não chegamos lá, eu sei. Insista e não desista, pois se fosse fácil todo mundo faria. Pode demorar, mas você vai conseguir se quiser.

Estou fazendo essa introdução melancólica e motivacional (hahaha), porque passei por um bom perrengue até encontrar a melhor alternativa para o meu caso, e isso levou (bastante) tempo. Passei os últimos 5 meses pesquisando sobre a solução, pedindo ajuda a amigos e comunidade (sem muito sucesso aqui, é importante ressaltar), efetuando uma série de testes e ao mesmo tempo dando conta das minhas demandas diárias que não podiam atrasar. Me vi só, perdido num deserto, sem bússola e sem mapa.

Na verdade, trabalhar em projetos que geram impacto no mercado é assim… É difícil mesmo!!! É desafiador e exigem de você muito esforço, determinação e dedicação. Mas pretendo falar sobre isso em outra publicação, para não me estender demais.

O problema de ter uma aplicação monolítica

Se você já sabe o que é e quais os problemas relacionados a uma aplicação monolítica, pode avançar para o próximo tópico. Caso contrário, leia este tópico e veja um pouco da minha opinião sobre esse tipo de aplicação.

Tá, vamos ao que realmente interessa. Primeiramente preciso falar um pouco do problema, para que depois fale da solução e de como o SSO me ajudou nesse caso.

Estou trabalhando num projeto que roda em cima de um sistema monolítico. Esse monolítico foi um MVP que teve uma série de avanços e retrocessos até chegar ao formato que chegou. Ele é o ponto central do negócio e possui regras fundamentais para o funcionamento da empresa. Profissionais mais experientes conseguem perceber claramente os malefícios de uma aplicação monolítica, principalmente quando é algo desenvolvido com versões antigas de frameworks, linguagens, pensamentos, conceitos e padrões.

Cito alguns aqui:

  • É altamente acoplada;
  • Custa (muito) caro manter, pois o requisito é alto e a aplicação é extremamente sensível;
  • Qualquer nova mudança pode (e na maioria das vezes vai) impactar em algo que já está funcionando;
  • Por serem legadas, dificilmente usam bons padrões e são testáveis. Só este ponto já merece um ponto final no texto inteiro (hahahaha), mas ainda tenho mais pra falar.

Embora seja fácil citar os pontos negativos, precisamos entender e respeitar o fato de que aplicações monolíticas ainda são comuns em Startups (que é o meu caso), por ex. Antes do “boom” fullstack, os padrões eram outros e aplicações monolíticas eram o caminho mais rápido para a entrega de um produto minimamente viável (leia-se: usável), num intervalo de tempo curto.

Diante desses aspectos, percebia que essa aplicação poderia virar uma bomba relógio, caso continuasse recebendo atualizações significativas. Nesse momento eu resolvi começar uma mudança silenciosa, mas importante para mim e para a empresa: trabalhar com Domain-Driven Design (DDD) e microsserviços. “Por quê, Mauricio?” Porque não fazia sentido ter, por ex., um sistema financeiro, dentro do sistema de artistas ou um sistema de notificações e/ou mensagens dentro do mesmo sistema do produtor de elenco. Eles fazem parte do domínio principal, mas são subdomínio diferentes; contextos diferentes… que erroneamente faziam parte da aplicação monolítica.

Não ficou claro? Vou dar outro exemplo… Faz sentido o sistema que recebe atualizações financeiras estar junto do CMS do site, por ex?

No meu ponto de vista, não. Entenda:

  • Eles tem responsabilidades diferentes;
  • são utilizados por pessoas diferentes;
  • são requisitados em tempos diferentes;
  • tem regras de negócio diferentes;
  • demandam recursos de máquina diferentes;
  • etc…

Separar esses contextos traz uma série de benefícios e algumas eu faço questão de citar:

  • Custa mais barato, embora pareça mais caro
    No aspecto infraestrutura, já fica fácil visualizar que o escalonamento da sua aplicação vai acontecer por partes e não integral. Não faz sentido escalar o cache, se o que está precisando ser escalado é o banco de dados.
  • O custo com mão de obra pode ser menor e mais direcionado
    Ora… Num monolítico o profissional vai custar bem mais caro, porque ele precisa manjar dos paranauês do canivete suíço. O cara tem que ser um semideus. De apertar porca com martelo pra cima. O mesmo não acontece quando se trabalha com microsserviços. Você contrata por especialidade. Seu analista especialista em finanças não vai estar lidando com questões relacionadas ao CMS. Com esse mesmo exemplo já fica claro ver que são requisitos diferentes de profissionais. Nesse caso, não seria impossível selecionar um júnior para o CMS e um sênior para o financeiro, por ex.
  • Separação das responsabilidades
    Seu contexto tem uma única responsabilidade: se preocupar com ele mesmo e são bem mais delimitados. Ele cuida dos próprios recursos e compete somente a ele o ônus e bônus dos seus resultados.

Esses são alguns dos muitos outros motivos que podem ser listados aqui.

Pensando e criando Microsserviços

Diante do problema que tínhamos com a aplicação monolítica, eu tinha o desafio de criar o primeiro microsserviço da plataforma. Comecei pelo sistema financeiro e vou explicar o porque.

Quando começamos o monolítico recebia todos os cadastros de usuários pelo backend, pois a plataforma só funcionava por meio de convites. Pouco tempo depois, depois que já tínhamos a anatomia da aplicação definida e operante, recebi a notícia de que iríamos passar a trabalhar com assinaturas.

Como citei no tópico anterior, não fazia sentido fazer toda a implementação financeira dentro do sistema monolítico, pelos motivos que citei (e muitos outros). Optei pelo Laravel e criei toda a aplicação para receber e gerenciar as assinaturas de cada novo usuário.

Aqui temos APIs, um backend (vue.js) que consome essas APIs. Contamos com Redis, RDS, algumas instâncias de EC2, Linode, e droplets da Digital Ocean para rodar tanto a aplicação em produção, quanto os testes, tudo orquestrado com Docker (ambiente de desenvolvimento, staging e deploy).

De um modo geral, o processo funciona da seguinte forma: A cada nova assinatura, o webhook dispara para o monolítico as informações referentes a assinatura recém criada e ele processa essa informação lá.

Esta arquitetura separou os processos, nos deu autonomia, mas ainda deixou o monolítico responsável pela autenticação do usuário assinante, o que era algo ruim. Isso aconteceu pelo fato de existirem assinantes convidados cadastrados. O sistema financeiro só recebia novos assinantes, mas os assinantes convidados continuavam no monolítico e isso era um problema. O monolítico não deveria ser mais responsável pelo login do assinante, já que tenho um sistema em outro local que controla a assinatura dele. Ou seja, tínhamos responsabilidades fora de contexto e precisávamos resolver.

E aí entrou em campo o SSO.

Single Sign-On (SSO)

Você pode não saber o que é SSO, mas com toda certeza já utilizou ferramentas que trabalham com este recurso. O Google é um exemplo. Você se autentica numa conta do Google e através de um único login, você consegue acessar todos os recursos que ele oferece (Gmail, Drive, Blogger, e todos os outros). A Amazon também possui o mesmo recurso. Pra quem tinha a conta da Amazon e passou a utilizar o AWS (como foi o meu caso) se surpreendeu ao ver as contas associadas. A Apple também oferece o mesmo recurso e muitas outras grandes empresas.

Single Sign-On é isso, é autenticar-se num único lugar e ter acesso a todos os recursos que aquela ferramenta pode te oferecer. E essa era a nossa necessidade. Quando decidimos migrar a autenticação do usuário para o sistema que controla as assinaturas, era indispensável não utilizar SSO, pois o assinante, ao mesmo tempo que pode acessar as informações financeiras dele, também pode editar o seu próprio perfil, acessar suas mensagens, visualizar suas notificações e acessar os conteúdos do CMS. Cada item desse é um microsserviço que requer autenticação para ser utilizado.

Trabalhando com Single Sign-On (SSO)

Pra mim, mais difícil que implementar foi montar o workflow. Decidir o que é melhor pro seu caso é difícil, principalmente quando a mudança claramente traz uma série de benefícios, mas gera um impacto significativo. Nesse caso o impacto foi pro assinante que obrigatoriamente precisou trocar suas senhas (não preciso dizer o porque, né? ¬¬).

Observe o diagrama sequencial na imagem abaixo, que eu vou explicar a seguir como cheguei a isso.

Single Sign-On Sequence Diagram

Vamos entender quem é quem, antes de continuar.

SSO Server

O SSO Server é o serviço responsável pela autenticação do usuário e por registra os Brokers. Somente os Brokers registrados podem tentar fazer login no SSO Server.

Broker

O Broker é a aplicação (pagina web, api, etc) que você quer permitir que o usuário acesse após se autenticar no SSO Server. Cada Broker possui um ID e uma chave privada que é utilizada para fazer a conexão entre o Broker e o SSO Server.

Quando uma nova requisição é feita por um Broker, este recebe do SSO Server um token referente a requisição, no ambiente em qual ele foi requisitado. Isso vai ficar mais claro logo abaixo.

Client

Client é quem deseja se autenticar no SSO Server para acessar conteúdos que são disponibilizados pelos Brokers.

Como funciona tudo isso?

Um cliente no nosso caso é um assinante. O assinante possui credenciais de acesso a aplicação e elas que são utilizadas para autenticar no SSO Server.

O assinante só pode se autenticar através de algum Broker. No exemplo que estamos utilizando, nós temos, então, dois Brokers:

  1. Broker App Financeiro (BAF)
  2. Broker App Site (BAS)

Quando um Client instancia um BAF via Browser, por ex., o Broker vai até o SSO Server e se autentica. Após se autenticar, ele devolve para o Client um Cookie com um hash único que o identifica, dentro daquela instância.

Aqui já posso sentir o cheirinho de dúvida. Você deve estar se perguntando sobre a performance disso, né?

Então… Se você observar bem o diagrama acima, o primeiro processo se refere à requisição de um conteúdo público. Pode ser algo como a página inicial do site. Ela não precisa de login e está disponível pra todo mundo. O mesmo acontece com as páginas institucionais, formulários de contato e até mesmo a página de login. Para casos que não precisam de identificação, nós não autenticamos o Broker no SSO Server, por que, tendo as aplicações na mesma rede ou não, esse é um típico esforço desnecessário. Logo, se o conteúdo é público, ele não precisa de autenticação e não envolve o SSO Server.

Mas se o conteúdo for privado, como você faz então?

Nesse caso, o SSO Server vai ser utilizado, mas um outro personagem entra em ação: o Broker Session Manager. Cada aplicação tem sua própria gerência de sessão. Se um Client requisitar um conteúdo privado, o Broker Session Manager é acionado para verificar se o Client possui sessão ativa.

Vamos entender fluxo, com um passo-a-passo.

Caso #1: Usuário não possui sessão ativa

  1. O cliente abre o browser e requisita um conteúdo privado;
  2. O Broker Session Manager entra e ação e verifica se o Client possui sessão ativa;
  3. Se não possuir, o Client é redirecionado para a página de login. Ele entra com as suas credenciais e submete o formulário;
  4. Nesse momento, duas autenticações acontecem: 
    1. O Broker se autentica no SSO Server; 
    2. O usuário se autentica pelo SSO Server. 
    Independente das credenciais do Client serem válidas, o SSO Server retorna para o Broker um token único daquela requisição, que por sua vez é salva num Cookie no navegador do Client.
  5. Se as credenciais forem válidas, o SSO Server retorna para o Broker um objeto com o Assinante. Considere isso como primeiro retorno e ele tem algumas informações importantes. A primeira é que ele contém dados da sessão que foi criada no SSO Server que contem o Objeto Assinante. A segunda é que ela contém m token de acesso que dá permissão para acessar a API do sistema financeiro, que também é um Broker, lembra? Ele é o Broker App Financeiro (BAF).
  6. O Broker, ao receber o Objeto do Assinante, procura por um cabeçalho do Assinante no próprio Broker. Esse cabeçalho é uma entidade de mapeamento. É como se também houvesse no Broker uma Assinantes com o ID do assinante no Broker e o ID do assinante do SSO Server. Isso nos permite a fazer algo como “Logar Como…” e é isso que o Broker faz. Depois de encontrar o assinante nele mesmo, ele internamente faz uma autenticação por ID. Só que nesse caso o ID utilizado é o ID do artista no SSO Server.
  7. Por fim, o Broker retorna para o Client Cookies que representam as duas sessões existentes para esse Client.

Caso #2: Autenticando em outro Broker

Vamos considerar aqui que agora queremos acessar o conteúdo do sistema de Mensagens (Broker App Mensagens — BAM). Para isso, o usuário precisa se autenticar também no sistema de mensagem, mas NÃO precisa se autenticar novamente no SSO Server, pois ele já possui uma sessão ativa com o SSO Server.

Logo, o processo é semelhante ao Caso #1, a diferença é que a única coisa que precisamos fazer é recuperar o Objeto Assinante (que o Client já tem) e forçar um novo login no sistema de Mensagens. Ou seja, só precisamos executar o passo 6 e 7 do Caso #1. Feito isso, temos um novo login também no Broker App Mensagens e o Client passa a ter um novo Cookie com informações do novo Broker.

Esse processo se repete para todos os demais Brokers.

Caso #3: Brokers que possuem formulário de login

Quando se trata de integração com APIs, é fácil de entender o processo, a autenticação forçada para acontecer internamente sem que o usuário perceba, etc. Mas quando temos dois Brokers que possuem formulário de login, como fazemos?

Esse é um dos grandes trunfos do SSO. Lembra que todos os Brokers são identificáveis no SSO Server? Lembra também que o Client, quando autenticado, possui uma Sessão ativa com o SSO Server? Então… É aqui que a mágica acontece.

Digamos que vc tem um Broker App Financeiro e outro com o seu CMS. Quando o usuário se autentica pelo Broker App Financeiro, no momento que ele requisitar algo no privado no CMS, o Broker Session Manager do CMS entrará em ação. Ai tudo acontece como disse no Caso #2 sem apresentar pro usuário um formulário de login. A autenticação acontece internamente. Simples assim.

Se o usuário iniciar a sessão no CMS e requisitar algo no Broker App Financeiro, o Broker Session Manager do Financeiro irá agir da mesma maneira e, dessa vez, é o formulário de login do Broker App Financeiro que não será apresentado pro usuário.

Isso significa que quando o Client se autentica em qualquer um dos Brokers, automaticamente ele estará autenticado no outro.

Dicas & Conclusão

Deixo aqui algumas dicas e considerações finais sobre a experiência vivenciada nesse processo.

  1. Pacote jasny/sso:
    O utilizei para gerenciar o SSO Server. Ele é bem completo, simples e bastante utilizado pela comunidade. Existem outros pacotes, inclusive encontrei um para Laravel (que parece ser um fork do jasny/sso), mas optei pelo jasny/sso por ver que há bastante tempo ele vem sendo utilizado e atualizado; pelo fato criador do pacote estar participando do processo de amadurecimento do pacote e participando ativamente das ISSUES. Como se trata de uma parte bem sensível, esse foi o que me deixou mais seguro. Existem outros, mas esse eu achei bem simples e interessante. Optei por ele pela quantidade de utilizações e pelo que vi nas ISSUES.
  2. Pacote JWT Auth:
    o JWT é stateless, um requisito indispensável pra trabalhar com SSO, já que um token não pode sobrescrever o outro, como acontece com a API nativa do Laravel.
  3. Pra trabalhar com esses pacotes precisei fazer algumas modificações em algumas partes dele. Por exemplo, no JWT eu criei um outro Route Middleware para validar o Token que faz um pouco mais do que o jwt.auth. Fiz isso porque a API funciona com autenticação por contexto. Eu posso permitir acesso pro usuário do backend (User model, por exemplo), como também posso permitir pro Assinante (Subscriber model, por exemplo). Esses dois contextos acessam recursos direrentes.
  4. Outra modificação foi no jasny/sso. Como utilizamos PHP-FPM, tivemos um problema com a função do PHP getallheaders() que é utilizada pelo pacote. Com isso precisei sobrescrever o método getBrokerSessionID() pra usar meu próprio método getallheaders().

Bom, acho que é isso. Espero que você tenha chegado até aqui o final, que tenha entendido os “Porquês?” das coisas e que esta publicação seja útil pra você também. Confesso que se tivesse encontrado algo desse gênero no começo deste processo, as coisas teriam sido um pouco mais fáceis pra mim. Por esse motivo fiz questão de dedicar algumas poucas horas para a escrita/revisão desta publicação.

Se não for pedir muito, curte aqui em baixo a publicação. Esse é um dos poucos parâmetros que tenho para saber sobre a relevância deste assunto.

Comente também. Pergunte, questione, fale. Aproveite. A informação que possuo é tão sua quanto minha. Basta perguntar que terei o prazer em ajudar.

Obrigado, um abraço e sucesso pra você!