Como desenvolver uma arquitetura de autenticação em Node.js e React.js

Um login inusitado como exemplo de logins em single page applications

Você trabalha com desenvolvimento web, conhece várias linguagens, seja ela PHP, Ruby ou até mesmo Node.js. Você também já conhece como implementar uma autenticação. No entanto, tenho uma triste notícia: você terá que reaprender a autenticar um usuário de novo! Para os que não tem experiência e está entrando agora no desenvolvimento, é hora de já aprender de um jeito novo a implementar login em aplicações web. Na era das single page applications, a arquitetura de sistemas com autenticações passou por transformações que teremos que recapitular a nossa forma de fazê-las para adaptar aos novos paradigmas de aplicações front-end modernas.

Autenticação do usuário usando sessão

A autenticação de um usuário logado em uma aplicação sempre foi feita de maneira simples e bem suportada em diversos frameworks usando a sessão no servidor. Como as páginas web são stateless, ou seja, elas não persistem o estado e precisam do banco de dados para armazenar qualquer coisa que precisa ser guardada, a sessão é uma solução para persistir dados que não são permanentes mas que dura exatamente uma sessão. Neste contexto também temos também a autenticação por Cookies e outras variações.

Autenticação em uma SPA (single page application)

Quando falamos de um sistema de autenticação e necessidade de saber se o usuário está logado em aplicações que persiste o estado da página, no contexto da web stateless, temos várias novas implementações para atender este contexto. Isto por que a forma de funcionamento dessas páginas não é tradicionalmente feita por links que levam a outras páginas, e que em cada requisição atualiza todo conteúdo e retorna todo o HTML gerado no servidor.

Uma página que não se atualiza teria que ter a sessão validada uma só vez, mas por outro lado, pelo fato dela não se atualizar e modificar o estado, ela precisa obter informações no servidor através de requisições, geralmente de API’s para obter dados e atualizar as partes específicas necessárias para dar o efeito desejado destas aplicações quase que “mágicas”, pois nestas single page applications podemos explorar efeitos e ter a sensação de que estamos em um aplicativo e não num amontoado de links e páginas que temos que esperar tudo carregar outra vez.

As chamadas por API precisam ser autenticadas de alguma forma, enviando na requisição informações da “sessão” do usuário. Destaquei a palavra sessão aqui, pois neste caso, a sessão não funcionaria, pois não estamos no servidor armazenando a sessão específica de um usuário. Numa arquitetura SPA, não temos como iniciar a sessão de um usuário só, pois para o servidor, as requisições feitas para obter dados não são identificadas. Em chamadas por API, o servidor não tem conhecimento da sessão, pois estas chamadas podem vir de qualquer lugar, e não necessariamente do mesmo servidor na mesma sessão.

Autenticando usuários com o Passport.js no Node.js com Sequelize, Express e JWT web token, armazenando no Local Storage em uma aplicação React no Front-end

Vamos usar esta combinação para realizar autenticações em uma aplicação React + Node.js.

Com isto, poderemos ter uma autenticação centralizada e que pode ser integrada com diversos serviços como o Google, Github, Bitbucket, Trello, Facebook e praticamente todas integrações suportadas pelo Passport.js.

O Passport é um framework de autenticação que se integra com os diferentes serviços acima, e pode ser usado de diversas formas, e neste caso usaremos aqui em conjunto com o JWT para autenticação do usuário através de web tokens.

O exemplo completo desta implementação pode ser encontrado no Github do Gitpay, onde está implementado o login usando este método.

Aqui iremos mostrar a integração com o Sequelize, mas que, com as devidas adaptações, pode funcionar para qualquer solução que você desejar usar para o banco de dados.

Passo 01 — Instalando as bibliotecas

Você precisa primeiramente instalar as bibliotecas citadas. Aqui, para simplicidade, vou considerar que uma aplicação em Node.js será desenvolvida com o express e que faremos um login com o Github. Sendo assim, temos:

npm install express passport passport-github2 passport-jwt

Passo 02 — Configurando a estratégia

O Passport.js tem o conceito de estratégias, que são modelos padronizados para implementar login em diferentes tipos de conta. Esta é uma das maiores vantagens de se usar o Passport, mas o que ele tem de facilidade para autenticar em serviços externos usando Oauth, ele tem de complicado para login local :p

Primeiro, serializamos o usuário e todas suas informações para poder ser obtida futuramente na requisição e o inverso também é realizado:

passport.serializeUser((user, done) => {
done(null, user)
})

passport.deserializeUser((user, done) => {
userExist(user).then(user => {
done(null, user)
})
})

Em seguida, como mostrado no código completo abaixo, criamos duas estratégias, uma chamada Local, para autenticação no próprio sistema, e outra usando o Github para realizar a integração do login com o serviço dele.

No modelo de estratégias, é também vantajoso por centralizar toda a lógica e usuários já existentes podem logar com diferentes contas e mesmo assim permanecer como o mesmo usuário.

Temos também uma estratégia JWT para armazenar o token que iremos usar para realizar a comunicação com segurança entre o front-end e a o back-end.

Passo 03 — Lógica para verificar as autenticações no servidor

Agora, temos funções utilitárias que serão chamadas em cada requisição feita pelas rotas, que será mostrado posteriormente, em que na requisição pegamos o token enviado e fazendo e usando os dados para verificar a autenticação do usuário.

Temos como base para qualquer requisição o envio de um token, que neste caso é um Bearer token e é extraído do cabeçalho de autenticação enviado e é manipulado para ser comparado com o token processado pelo JWT

function isAuth (req, res, next) {
// if (req.isAuthenticated()) return res.send({ 'authenticated': true });

const token = req.headers.authorization.split(' ')[1]

if (token) {
return jwt.verify(token, process.env.SECRET_PHRASE, (err, decoded) => {
// the 401 code is for unauthorized status
if (err) {
return res.status(401).end()
}

const userData = decoded
// check if a user exists
return userExist(userData).then(user => {
return res.send({ authenticated: true, user: user })
}).catch(e => {
// eslint-disable-next-line no-console
console.log('error to sign user')
return res.status(401).end()
})
})
}
return next()
}

Então vemos se o token é válido e se existe um usuário associado a ele para validar todas as rotas.

Passo 04 — Autenticação das rotas

Quando chegamos na rota, importamos os métodos de autorização para serem usados para autenticar as rotas em uma aplicação que roda pelo Express neste exemplo.

Neste caso também tivemos que personalizar melhor as rotas de redirecionamento para usuários autenticados, passando o token nesta requisição para o front-end numa rota já que poderá ser entendia pelo React.

Passo 05 — Autenticação no front-end com o LocalStorage

No front-end precisamos do LocalStorage para armazenar o token e partir dele enviar todas requisições com autenticação depois que o usuário está logado.

Passo 06 — Implementação no front-end com o React Router e componente de Autenticação

O React Router entenderá o token sendo enviado para uma rota especifica através de um componente de sessão, que verifica o LocalStorage e para direcionar para novas rotas, funcionando como um inicializador de sessão.

Como realizar testes automatizados

Os testes automatizados são muito importantes neste caso, pois você consegue desenhar melhor suas rotas de login e descrever por testes diferentes casos, para não passar maiores problemas quando for integrar com o front-end.

Os testes basicamente, para rotas de autenticação externas, envia tokens autenticados e simula uma autenticação correta e com erros caso não tenha o cabeçalho de autorização válidos:

describe('login User social networks', () => {
it('should user authenticated', (done) => {
agent
.get('/authenticated')
.set('authorization', 'Bearer token-123') // 1) using the authorization header
.expect('Content-Type', /json/)
.expect(401)
.end((err, res) => {
expect(res.statusCode).to.equal(401);
done();
})
})

it('should user google', (done) => {
agent
.get('/authorize/google')
.send({email: 'teste@gmail.com', password: 'teste'})
.expect(302)
.end((err, res) => {

expect(res.statusCode).to.equal(302);
expect(res.headers.location).to.include('url from provider')
done();
})
})

Se quiser ver os testes completos, você pode ver o código fonte no Gitpay dos nossos testes de usuário automatizados.

Considerações

Algumas considerações importantes na hora de implementar a autenticação é ter idéia da estratégia que você irá usar para o seu tipo de aplicação. Em uma aplicação puramente Rails, por exemplo, eu não precisarei de um token se não for uma aplicação de uma página.

E até mesmo para este contexto específico, das SPA’s, temos diversas formas de implementar e aqui foi uma delas, usando o token JWT e no front-end armazenando com LocalStorage. Existem muitos artigos e discussões sobre as diferentes formas, com diferentes níveis de segurança. Há quem diga que desta forma é inseguro, mas depois de toda pesquisa julguei ser seguro o suficiente, pois o token fica armazenado somente para o usuário. Existem formas como armazenar no banco de dados e também diferentes restrições, como implementações em que o token invalida em questão de minutos, sendo necessário renovar constantemente e fazer o gerenciamento disto. É preciso avaliar o nível de segurança suficiente para suas necessidades.

Conclusão

Abordamos uma forma de autenticar em Single Page Applications, que requer estratégias diferentes do modelo em que o desenvolvimento web vinha abordado, isto por que temos agora aplicações de uma única sessão, onde o usuário não atualiza a tela e requisições são feitas de forma independente. Neste caso, temos também a separação das requisições front-end e back-end através de API’s, que permitem uma comunicação multiplataforma e com uma autenticação usando Oauth e uma combinação de novas formas de autenticar neste contexto.

Para isto, precisamos saber unir as seguintes bibliotecas para realizar o fluxo da autenticação:

  1. Node.js: O Node é o servidor Javascript usado neste exemplo onde fica o back-end da aplicação
  2. Express.js: Uma das bibliotecas mais conhecidas do Node, ela tem facilidades para criação de um servidor e manipulação de rotas
  3. Oauth: Framework para autenticação disponível em diversas linguagens
  4. JWT: Abreviação para JSON Web Token, é o tipo de implementação do token que vamos usar para comunicar-mos entre o front-end e o back-end através de um token válido que será usado durante a sessão
  5. LocalStorage: Este token é enviado uma única vez para a aplicação para que seja gravado no Browser do usuário através do LocalStorage, uma API do Dom implementada nos Browsers para armazenar informações. Quando o LocalStorage é apagado, o usuário precisa logar novamente, e o mesmo acontece para sessões.

Para qualquer nova aplicação geralmente é necessário existir a autenticação e é uma das implementações mais cuidadosa a ser feita, pois lida com dados sensíveis dos seus usuários tanto na sua aplicação, quanto em serviços externos.

Se quiser aprender mais e ver como funciona em um projeto real, cadastre-se no Gitpay e veja nosso código fonte.