Back-End

3 jul, 2018

Autenticação JSON Web Token (JWT) em Node.js

Publicidade

Faz algum tempo que ensino como fazer APIs em Node.js, uma vez que este é o cenário mais comum de uso com a plataforma.

Já ensinei a fazer APIs com MongoDB, MySQL e SQL Server. Já ensinei também a estruturar APIs em formato de microsserviços e como criar um API Gateway.

No entanto, pouco falei aqui sobre segurança em APIs. O mais próximo disso é o artigo de Passport e as dicas que dei sobre usar o pacote Helmet em alguns artigos.

Hoje falaremos de JSON Web Tokens como uma forma de garantir a autenticação e autorização de uso de APIs de maneira bem simples e segura, sendo o JWT um padrão para segurança de APIs RESTful atualmente.

JSON Web Tokens

JWT, resumidamente, é uma string de caracteres codificados que, caso cliente e servidor estejam sob HTTPS, permite que somente o servidor que conhece o ‘segredo’ possa ler o conteúdo do token, e assim confirmar a autenticidade do cliente.

Ou seja, quando um usuário se autentica no sistema (com usuário e senha), o servidor gera um token com data de expiração pra ele. Durante as requisições seguintes do cliente, o JWT é enviado no cabeçalho da requisição e, caso esteja válido, a API irá permitir acesso aos recursos solicitados, sem a necessidade de se autenticar novamente.

O diagrama abaixo mostra este fluxo, passo a passo:

Fluxo JWT

O conteúdo do JWT é um payload JSON que pode conter a informação que você desejar, que lhe permita mais tarde conceder autorização a determinados recursos para determinados usuários. Minimamente ele terá o ID do usuário autenticado, mas pode conter muito mais do que isso.

Estruturando a API

Antes de começarmos esta API Node.js usando JWT, vale ressaltar que o foco aqui é mostrar o funcionamento do JWT e não o funcionamento de uma API real.

Não focaremos no processo de autenticação inicial, que pode tranquilamente ser feito usando Passport ou diretamente com user/password batendo no servidor. Vamos mockar os dados de autenticação inicial para ir logo para a geração e posterior verificação dos tokens.

Para não termos de começar a configurar uma API do zero, recomendo baixar os fontes do artigo de API Gateway, uma vez que o API Gateway é uma API bem simples, com apenas um arquivo index.js. Caso não queira ter de subir as APIs fake às quais ele depende, basta substituir o código de proxy por um retorno JSON fake que não vai fazer diferença nenhuma para este passo a passo.

Você deve ter o seguinte index.js do API Gateway em mãos:

//index.js
var http = require('http');
const express = require('express')
const httpProxy = require('express-http-proxy')
const app = express()
var cookieParser = require('cookie-parser');
var logger = require('morgan');
const helmet = require('helmet');

const userServiceProxy = httpProxy('http://localhost:3001');
const productsServiceProxy = httpProxy('http://localhost:3002');

// Proxy request
app.get('/users', (req, res, next) => {
  userServiceProxy(req, res, next);
})

app.get('/products', (req, res, next) => {
  productsServiceProxy(req, res, next);
})

app.use(logger('dev'));
app.use(helmet());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

var server = http.createServer(app);
server.listen(3000);

Com esse JS em mãos, vamos instalar algumas novas dependências na nossa aplicação que nos permitirão o uso de JWT:

npm install jsonwebtoken dotenv-safe

A saber:

  • jsonwebtoken: pacote que implementa o protocolo JSON Web Token;
  • dotenv-safe: pacote para gerenciar facilmente variáveis de ambiente;

Vamos começar com o dotenv-safe, criando dois arquivos ocultos. Primeiro, o arquivo .env.example, com o template de variáveis de ambiente que vamos precisar:

# .env.example, commit to repo
SECRET=

E depois, o arquivo .env, com o valor do segredo à sua escolha:

#.env, don't commit to repo
SECRET=mysecret

Este segredo será utilizado pela biblioteca jsonwebtoken para criptografar o token de modo que somente o servidor consiga lê-lo, então é de bom tom que seja um segredo forte.

Para que esse arquivo de variáveis de ambiente seja carregado assim que a aplicação iniciar, adicione a seguinte linha logo no início do arquivo index.js da sua API, aproveitando para inserir também as linhas dos novos pacotes que vamos trabalhar:

require("dotenv-safe").load();
var jwt = require('jsonwebtoken');

Isso deixa nossa API minimamente preparada para de fato lidar com a autenticação e autorização.

Autenticação e autorização

Caso não saiba a diferença, autenticação é você provar que você é você mesmo. Já autorização, é você provar que possui permissão para fazer ou ver o que você está tentando.

Antes de emitir o JWT, é necessário que o usuário passe por uma autenticação tradicional, geralmente com usuário e senha. Essa informação fornecida é validada junto a uma base de dados e somente caso ela esteja ok é que geramos o JWT para ele.

Assim, vamos criar uma nova rota /login que vai receber um usuário e senha hipotético e, caso esteja ok, retornará um JWT para o cliente:

//authentication
app.post('/login', (req, res, next) => {
  if(req.body.user === 'luiz' && req.body.pwd === '123'){
    //auth ok
    const id = 1; //esse id viria do banco de dados
    var token = jwt.sign({ id }, process.env.SECRET, {
      expiresIn: 300 // expires in 5min
    });
    res.status(200).send({ auth: true, token: token });
  }
  
  res.status(500).send('Login inválido!');
})

Aqui temos o seguinte cenário: o cliente posta na URL /login um user e um pwd, que simulo uma ida ao banco meramente verificando se user é igual a luiz e se pwd é igual a 123. Estando ok, o banco me retornaria o ID deste usuário, que simulei com uma constante.

Esse ID está sendo usado como payload do JWT que está sendo assinado, mas poderia ter mais informações conforme a sua necessidade. Além do payload, é passado o SECRET, que está armazenado em uma variável de ambiente como mandam as boas práticas de segurança. Por fim, adicionei uma expiração de cinco minutos para esse token, o que quer dizer que o usuário autenticado poderá fazer suas requisições por cinco minutos antes do sistema pedir que ele se autentique novamente.

Caso o user e pwd não coincidam, será devolvido um erro ao usuário.

Vamos aproveitar o embalo e vamos criar uma rota para o logout:

app.get('/logout', function(req, res) {
  res.status(200).send({ auth: false, token: null });
});

Aqui apenas anulamos o token, embora esta rota de logout seja completamente opcional, uma vez que no próprio client-side é possível destruir o cookie de autenticação e com isso o usuário está automaticamente deslogado.

Mas será que está funcionando?

Aí que entra a autorização!

Vamos criar uma função de verificação em nosso index.js, com o intuito de, dada uma requisição que está chegando, a gente verifica se ela possui um JWT válido:

function verifyJWT(req, res, next){
  var token = req.headers['x-access-token'];
  if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' });
  
  jwt.verify(token, process.env.SECRET, function(err, decoded) {
    if (err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });
    
    // se tudo estiver ok, salva no request para uso posterior
    req.userId = decoded.id;
    next();
  });
}

Aqui eu obtive o token a partir do cabeçalho x-access-token, que se não existir já gera um erro logo de primeira.

Caso exista, verificamos a autenticidade desse token usando a função verify, usando a variável de ambiente com o SECRET. Caso ele não consiga decodificar o token, irá gerar um erro. Em seguida, chamamos a função next que passa para o próximo estágio de execução das funções no pipeline do middleware do Express, mas não antes de salvar a informação do id do usuário para a requisição, visando poder ser utilizado pelo próximo estágio.

Ok, entendi que esta função atuará como um middleware, mas como usaremos a mesma?

Basta inserirmos sua referência nas chamadas GET que já existiam em nosso API Gateway:

// Proxy request
app.get('/users', verifyJWT, (req, res, next) => {
  userServiceProxy(req, res, next);
})

app.get('/products', verifyJWT, (req, res, next) => {
  productsServiceProxy(req, res, next);
})

Assim, antes de redirecionar os GETs para as APIs de destino, o API Gateway vai criar essa camada intermediária de autorização baseada em JWT, que obviamente vai bloquear requisições que não estejam autenticadas e autorizadas, conforme suas regras para tal.

O resultado, tentando chamar a rota /users sem estar autenticado, é esse:

JWT Inexistente

Agora, se realizarmos a autenticação usando POSTMAN:

Login via POSTMAN

Conseguiremos fazer novas chamadas aos endpoints da API, pois agora temos o token JWT para passar no header da nossa requisição (x-access-token):

Requisição realizada com sucesso

E por fim, como este token está programado para expirar cinco minutos após a sua criação, as requisições podem ser feitas usando o mesmo durante este tempo, sem novo login. Mas assim que ele expirar, receberemos como retorno.

Token expirado

Bacana, não?

E com isso encerro o artigo de hoje. Note que foquei no uso do JWT, sem entrar em muitos detalhes sobre como você pode estruturar sua autenticação inicial (login e senha) e nem como você pode estruturar o seu modelo de autorização (terá perfis de acesso, por exemplo?).

Quem sabe não abordo este tema de maneira mais avançada e completa em outra oportunidade.

Um abraço e até mais!