Artigo original: Securing Node.js RESTful APIs with JSON Web Tokens

Você já se perguntou como funciona uma autenticação? O que está por trás de toda a complexidade e abstrações. Para falar a verdade, não há nada de especial. É apenas uma maneira de criptografar um valor, criando um token exclusivo que os usuários utilizam como identificador. Este token verifica sua identidade. Ele pode autenticar quem você é e autorizar vários recursos aos quais você tem acesso. Se por acaso você não conhece nenhuma dessas palavras-chaves que falei, tenha paciência, vou explicar tudo isso.

Este será um tutorial passo a passo de como adicionar uma autenticação baseada em token em uma API REST existente. A estratégia de autenticação em questão é o JWT (JSON Web Token). Se isso não lhe diz muito, tudo bem. Foi bem estranho para mim quando ouvi pela primeira vez esse termo.

O que JWT realmente significa em um ponto de vista realista? Vamos detalhar o que a definição oficial afirma:

JSON Web Token (JWT) é um meio compacto e seguro para que o URL represente declarações a serem transferidas entre duas partes. As declarações no JWT são codificadas como um objeto JSON que é usado como conteúdo de uma estrutura JSON Web Signature (JWS) ou como um texto simples de uma estrutura JSON Web Encryption (JWE), permitindo que as declarações sejam assinadas digitalmente ou protegidas por integridade com um Código de Autenticação de Mensagem (MAC) e/ou criptografado.
- Internet Engineering Task Force (IETF)

Isso foi bem técnico. Agora vamos traduzir isso para o português. Um JWT é uma cadeia de caracteres codificada de forma segura para ser enviada entre dois computadores se ambos tiverem HTTPS. O token representa um valor acessível apenas pelo computador que tem acesso à chave secreta com a qual foi criptografada. Ficou mais simples, certo?

Qual é a aparência disso na vida real? Digamos que um usuário queira fazer login em sua conta. Ele envia uma solicitação (request) com as credenciais necessárias, como e-mail e senha, para o servidor. O servidor verifica se as credenciais são válidas. Se forem, o servidor cria um token usando a informação real que se quer transmitir (também chamada de payload) e uma chave secreta. Essa sequência de caracteres resultante da criptografia é chamada de token. Após a criação do token, o servidor o envia de volta para o client. O client, por sua vez, salva o token para usá-lo em todas as outras solicitações que o usuário enviar. A prática de adicionar um token aos cabeçalhos da solicitação é uma forma de autorizar o usuário a acessar os recursos. Este é um exemplo prático de como o JWT funciona.

Ok, mas chega de conversa! O restante do tutorial será de codificação, e eu adoraria que você acompanhasse e codificasse junto, à medida que progredimos. Cada trecho de código será seguido por uma explicação. Eu acredito que a melhor forma de entender corretamente será você codificar por si mesmo ao longo do caminho.

Antes de começar, porém, há algumas coisas que você precisa saber sobre o Node.js e alguns padrões do EcmaScript que usarei. Não vou usar a ES6, pois não é tão amigável para iniciantes quanto o JavaScript tradicional. Espero, no entanto, que você já saiba como construir uma API RESTful com o Node.js. Se não, você pode fazer um desvio e conferir este artigo (em inglês) antes de prosseguirmos.

Além disso, toda a demonstração deste artigo está no GitHub caso você queira o código completo.

Vamos começar a escrever o código?

Bem, ainda não, na verdade. Precisamos configurar o ambiente primeiro. O código terá que esperar mais alguns minutos. Esta parte é mais chatinha. Então, para começar mais rápido, vamos clonar o repositório do tutorial acima. Abra o terminal ou o prompt de linha de comando na pasta que desejar e execute este comando:

git clone https://github.com/adnanrahic/nodejs-restful-api.git

Você verá uma pasta aparecer. É só abri-la. Agora, vamos dar uma olhada na estrutura de pastas.

> user
  - User.js
  - UserController.js
- db.js
- server.js
- app.js
- package.json

Temos uma pasta de usuário com um modelo e um controlador, além de um CRUD básico já implementado. Nosso app.js já contém a configuração básica. O db.js garante que o aplicativo se conecte ao banco de dados e o server.js garante o funcionamento do nosso servidor.

Instale todos os módulos do Node necessários. Ainda no terminal, verifique se você está na pasta chamada nodejs-restful-api e execute npm install. Espere alguns segundos para que os módulos sejam instalados. Agora, precisaremos adicionar o URL de conexão de banco de dados no arquivo db.js.

Entre no mLab, crie uma conta se ainda não tiver feito isso, e abra o painel do banco de dados. Crie um novo banco de dados, que você pode nomear como quiser, e depois prossiga para a página de configuração. Adicione um usuário no seu banco de dados e copie o URL de conexão do painel para seu código.

Z5HFF8CQYR9cwiRY8f08tlWbz8PQARSVDHGL

Tudo o que você precisa fazer agora é alterar os valores de placeholder para <dbuser> e <dbpassword>. Substitua-os pelo nome de usuário e senha do usuário que você criou para o banco de dados. Uma explicação mais detalhada desse passo a passo pode ser encontrada no tutorial vinculado acima (em inglês).

Digamos que o usuário que criei para o banco de dados seja chamado de wally, com a senha theflashisawesome. Com isso em mente, o arquivo db.js deve ter esta aparência:

var mongoose = require('mongoose');
mongoose.connect('mongodb://wally:theflashisawesome@ds147072.mlab.com:47072/securing-rest-apis-with-jwt', { useUnifiedTopology: true, useNewUrlParser: true });

Agora, ative o servidor, digitando no terminal node server.js. Deve aparecer a frase Express server listening on port 3000 se a conexão foi estabelecida corretamente.

Finalmente, o código.

Vamos começar fazendo um brainstorming sobre o que nós queremos construir. Primeiro, precisamos adicionar a autenticação do usuário, ou seja, vamos implementar um sistema de registro e login de usuários.

Depois, queremos adicionar a autorização do usuário, que é o ato de conceder aos usuários a permissão para acessar determinados recursos em nossa API REST.

Comece adicionando um novo arquivo no diretório raiz do projeto. Dê a ele o nome de config.js, pois é aqui você colocará as definições de configuração para o aplicativo. Tudo o que precisamos no momento é apenas definir uma chave secreta (que chamaremos de secret) para nosso JSON Web Token.

ATENÇÃO: Tenha em mente que, sob nenhuma circunstância (NUNCA!), sua chave secreta deve ficar visível publicamente. SEMPRE coloque todas as suas chaves em variáveis ​​de ambiente! Estou apenas escrevendo-as dessa forma aqui para fins de demonstração.

// config.js
module.exports = {
  'secret': 'supersecret'
};

Com ela adicionada, já podemos começar a adicionar a lógica da autenticação. Crie uma pasta chamada auth e adicione um arquivo chamado AuthController.js. Este controlador será onde ficará nossa lógica de autenticação.

Adicione o seguinte código na parte superior do arquivo AuthController.js.

// AuthController.js

var express = require('express');
var router = express.Router();
var bodyParser = require('body-parser');
router.use(bodyParser.urlencoded({ extended: false }));
router.use(bodyParser.json());
var User = require('../user/User');

Agora, vamos adicionar os módulos para usar o JSON Web Tokens e criptografar as senhas. Cole este código no AuthController.js:

var jwt = require('jsonwebtoken');
var bcrypt = require('bcryptjs');
var config = require('../config');

Abra o terminal na pasta do seu projeto e instale os seguintes módulos:

npm install jsonwebtoken --save
npm install bcryptjs --save

Esses são todos os módulos de que precisaremos para implementar nossa autenticação. Agora, você está pronto para criar o /register que é um endpoint (local onde haverá a conexão junto ao servidor). Adicione este código ao seu AuthController.js:

router.post('/register', function(req, res) {
  
  var hashedPassword = bcrypt.hashSync(req.body.password, 8);
  
  User.create({
    name : req.body.name,
    email : req.body.email,
    password : hashedPassword
  },
  function (err, user) {
    if (err) return res.status(500).send("Ocorreu um problema ao registrar o usuário.")
    // criar o token
    var token = jwt.sign({ id: user._id }, config.secret, {
      expiresIn: 86400 // expira em 24 horas
    });
    res.status(200).send({ auth: true, token: token });
  }); 
});

Aqui, esperamos que o usuário nos envie três valores: um nome, um e-mail e uma senha. Pegaremos imediatamente a senha e vamos criptografá-la com o método de hash do Bcrypt. Em seguida, pegaremos a senha com hash, incluiremos o nome e o e-mail e criaremos um novo usuário. Após a criação bem-sucedida do usuário, podemos criar um token para ele.

O método jwt.sign() usa o payload e a chave secreta definida em config.js como parâmetros. Ele cria uma sequência exclusiva de caracteres representando o payload que, no nosso caso, é um objeto que contém apenas o id do usuário. Nesse momento, escrevemos a parte do código que obterá o ID do usuário com base no token que recebemos do endpoint register, o qual criamos anteriormente.

router.get('/me', function(req, res) {
  var token = req.headers['x-access-token'];
  if (!token) return res.status(401).send({ auth: false, message: 'Nenhum token informado.' });
  
  jwt.verify(token, config.secret, function(err, decoded) {
    if (err) return res.status(500).send({ auth: false, message: 'Falha ao autenticar o token.' });
    
    res.status(200).send(decoded);
  });
});

Aqui, esperamos que o token seja enviado junto com a solicitação nos cabeçalhos. O nome padrão para um token nos cabeçalhos de uma solicitação HTTP é x-access-token. Se não houver nenhum token fornecido com a request, o servidor retornará um erro. Para ser mais preciso, um status 401 unauthorized, de 'não autorizado', com a mensagem de resposta do servidor (que é chamada de response) 'Nenhum token informado'. Se o token existir, o método jwt.verify() será chamado. Esse método decodifica o token que possibilita a visualização do payload original. Trataremos de erros se houver algum. Se não houver, enviaremos de volta o valor decodificado como response.

Por fim, precisaremos adicionar essa rota ao AuthController.js em nosso arquivo principal app.js. Primeiro, exporte o router como módulo de AuthController.js:

// adicione isso no final do arquivo AuthController.js
module.exports = router;

Em seguida, adicione uma referência ao controlador no aplicativo principal, logo acima de onde você exportou o aplicativo.

// app.js
var AuthController = require('./auth/AuthController');
app.use('/api/auth', AuthController);
module.exports = app;

Agora, vamos testar isso. Por que não?

Abra a ferramenta de teste de API REST que você usa. Eu uso o Postman ou o Insomnia, mas qualquer um serve.

Volte para o seu terminal e execute node server.js. Só lembrando que, se já estiver em execução, sempre que alterar o código, pare-o, salve todas as alterações feitas nos arquivos e execute node server.js novamente.

Abra o Postman e crie uma request para o localhost, na porta 3000 (que é a porta em que está o nosso server), com o endpoint do registro (/api/auth/register). Certifique-se de escolher o método POST e o formato do Body x-www-form-url-encoded. Agora, adicione os valores. O nome do meu usuário é Mike e a senha é 'thisisasecretpassword'. Sei que não é a melhor senha que já vi, para ser honesto, mas serve. Então clique em enviar!

vshJBCITdZGicsO1sw5prUHlSGR9bNugRtjv
/register

Viu o que retornou? O token é uma string longa e confusa. Para experimentar o endpoint /api/auth/me, primeiro copie o token. Altere o URL para /me em vez de /register e o método para GET. Agora, você pode adicionar o token ao cabeçalho (headers) da request como valor da chave x-access-token.

Lh6dOAOqy3A5tpOPy8xUQLKONDUsVmT-uVup
/me

Aí está! O token foi decodificado em um objeto com um campo id. Quer ter certeza de que o id realmente pertence a Mike, o usuário que acabamos de criar? Com certeza. Então, volte para o seu editor de código.

// no AuthController.js mude essa linha:
res.status(200).send(decoded);

// para isso:
User.findById(decoded.id, function (err, user) {
  if (err) return res.status(500).send("Ocorreu um problema ao encontrar o usuário.");
  if (!user) return res.status(404).send("Usuário não encontrado.");
  
  res.status(200).send(user);
});

Agora, ao enviar uma solicitação para o endpoint /me, você verá:

JfGjoyqG9zttRCEPsXTkUiNQ12NbKbRiqzyy

A response agora contém todo o objeto de usuário! Bacana, hein👍! Só que não😒. A senha nunca deve ser retornada com os outros dados sobre o usuário. Então, vamos consertar isso. Podemos adicionar uma projeção à consulta e omitir a senha. Assim:

User.findById(decoded.id, 
  { password: 0 }, // projeção
  function (err, user) {
    if (err) return res.status(500).send("Ocorreu um problema ao encontrar o usuário.");
    if (!user) return res.status(404).send("Usuário não encontrado.");
    
    res.status(200).send(user);
});
GSTNoNziXzH43m9kEYX499bgB012YenWiHAF

Agora, está muito melhor, pois conseguimos ver todos os valores, exceto a senha.

Alguém falou em login?

Depois de implementar o registro, devemos criar uma forma de login dos usuários existentes. Vamos pensar um pouco sobre isso. O endpoint de registro exigia que criássemos um usuário, criássemos uma senha e emitíssemos um token. O que precisamos implementar no endpoint de login? Ele deve verificar se existe um usuário com o e-mail fornecido. Além disso, é preciso verificar se a senha fornecida corresponde à senha com hash no banco de dados. Só então vamos querer emitir um token. Adicione isso ao seu AuthController.js.

router.post('/login', function(req, res) {

  User.findOne({ email: req.body.email }, function (err, user) {
    if (err) return res.status(500).send('Erro no servidor.');
    if (!user) return res.status(404).send('Usuário não encontrado.');
    
    var passwordIsValid = bcrypt.compareSync(req.body.password, user.password);
    if (!passwordIsValid) return res.status(401).send({ auth: false, token: null });
    
    var token = jwt.sign({ id: user._id }, config.secret, {
      expiresIn: 86400 // expira em 24 horas
    });
    
    res.status(200).send({ auth: true, token: token });
  });
  
});

Em primeiro lugar, verificamos se o usuário existe. Depois, usando o método .compareSync()do Bcrypt,  comparamos a senha enviada na request com a senha no banco de dados. Se forem iguais, enviamos um token com .sign(). É quase isso. Vamos testar.

sSJkpDDrhTNNLeYSG-b0MD5gfQBFqNMK6uUx

Está funcionando! Mas, e se errarmos a senha?

QkXrnEyXUKhakmAGysGEIGVd39S3uLmhncLE

Ótimo, pois quando a senha está errada o servidor retorna o status 401 unauthorized. Exatamente o que queremos!

Para finalizar esta parte do tutorial, vamos adicionar um endpoint de logout simples para anular o token.

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

Atenção: o endpoint de logout não é necessário. E o ato de sair só pode ser feito pelo lado do client. Um token geralmente é mantido em um cookie ou no armazenamento local do navegador, por isso o logout é tão simples quanto destruir o token no client. Esse endpoint /logout é criado para representar logicamente o que acontece quando você faz logout, ou seja, definindo o token como null.

Com isso finalizamos a parte de autenticação do tutorial. Agora, vamos passar para a autorização? Eu aposto que você está louco para ver isso.

Você tem permissão para estar aqui?

Para compreender a lógica por trás de uma estratégia de autorização, precisamos entender algo chamado middleware. Em inglês, esse nome pode até ser autoexplicativo, mas traduzimos ele literalmente para o português como 'artigo do meio/intermediário'. De forma simples, um Middleware é um código pequeno, uma função no Node.js, que atua como ponte entre duas partes de um código.

Quando uma request atinge um endpoint, o router tem a opção de passar ela para a próxima (next) função do middleware. Ênfase na palavra next! Porque esse é exatamente esse o nome da função! Vamos ver um exemplo. Comente a linha em que você retorna o usuário como response, e adicione um next(user) logo abaixo.

router.get('/me', function(req, res, next) {
    
  var token = req.headers['x-access-token'];
  if (!token) return res.status(401).send({ auth: false, message: 'Nenhum token informado.' });
  
  jwt.verify(token, config.secret, function(err, decoded) {
    if (err) return res.status(500).send({ auth: false, message: 'Falha ao autenticar o token.' });
    
    User.findById(decoded.id, 
    { password: 0 }, // projeção
    function (err, user) {
      if (err) return res.status(500).send("Ocorreu um problema ao encontrar o usuário.");
      if (!user) return res.status(404).send("Usuário não encontrado.");
        
      // res.status(200).send(user); Comente essa linha
      next(user); // adicione essa linha
    });
  });
});

// adicione a função middleware
router.use(function (user, req, res, next) {
  res.status(200).send(user);
});
As funções de middleware são funções que têm acesso ao objeto request (req), ao objeto response (res) e à função next no ciclo de request-response do aplicativo. A função next é uma função no Express router que, quando chamada, executa o middleware depois do middleware atual.
- Using middleware, expressjs.com

Volte para o Postman e confira o que acontece quando você atinge o endpoint /api/auth/me. Surpreende que o resultado seja exatamente o mesmo? Se não surpreendeu, deveria!

Atenção: exclua este exemplo antes de continuarmos, pois ele é usado apenas para demonstrar a lógica do uso do next().

Agora vamos pegar essa mesma lógica e aplicá-la para criar uma função de middleware para verificar a validade dos tokens. Então, crie um arquivo na pasta auth e coloque o nome de VerifyToken.js e cole este trecho de código nele.

var jwt = require('jsonwebtoken');
var config = require('../config');

function verifyToken(req, res, next) {
  var token = req.headers['x-access-token'];
  if (!token)
    return res.status(403).send({ auth: false, message: 'Nenhum token informado.' });
    
  jwt.verify(token, config.secret, function(err, decoded) {
    if (err)
    return res.status(500).send({ auth: false, message: 'Falha ao autenticar o token.' });
      
    // se tudo der certo, salve para a request para uso em outras rotas
    req.userId = decoded.id;
    next();
  });
}

module.exports = verifyToken;

Vamos decompô-lo. Estamos usando essa função como um middleware personalizado para verificar se existe um token e se ele é válido. Após validá-lo, adicionamos o valor de decoded.id à variável request (req). Agora, temos acesso a ele na próxima função do ciclo de request-response. A chamada a next() garantirá que o fluxo continue para a próxima função em espera na fila. No final, exportamos a função.

Agora, abra o AuthController.js mais uma vez. Adicione uma referência a VerifyToken.js no início do arquivo e edite o endpoint /me. Deve ficar assim:

// AuthController.js

var VerifyToken = require('./VerifyToken');

// ...

router.get('/me', VerifyToken, function(req, res, next) {

  User.findById(req.userId, { password: 0 }, function (err, user) {
    if (err) return res.status(500).send("Ocorreu um problema ao encontrar o usuário.");
    if (!user) return res.status(404).send("Usuário não encontrado.");
    
    res.status(200).send(user);
  });
  
});

// ...

Viu como adicionamos VerifyToken na cadeia de funções? Nós agora estamos tratando de toda a autorização no middleware. Isso libera todo o espaço na callback para lidar apenas com a lógica de que precisamos. Este é um exemplo incrível de como escrever um código DRY (sem repetições desnecessárias). Agora, toda vez que você precisar autorizar um usuário, pode adicionar essa função middleware à cadeia. Teste no Postman novamente, para ter certeza de que está funcionando corretamente.

19EpKwBFR9RiWF4P-lDTJYRIiCYsCynW9bU7

Sinta-se à vontade para mexer no token e testar o endpoint novamente. Com um token inválido, você verá a mensagem de erro esperada e terá certeza de que o código que escreveu funciona da maneira desejada.

Por que isso é tão poderoso? Porque você pode adicionar agora o middleware VerifyToken em qualquer cadeia de funções e certificar-se de que os endpoints são seguros. Somente usuários com tokens verificados podem acessar os recursos!

Uma visão geral sobre o que acabamos de ver

Não se sinta mal se você não entendeu tudo de uma vez. Alguns desses conceitos são difíceis de entender mesmo. Não há problema em dar um passo para trás e descansar seu cérebro antes de tentar novamente. É por isso que eu recomendo que você analise o código sozinho e tente o seu melhor para fazê-lo funcionar.

Novamente, aqui está o repositório do GitHub . Você pode ver ali qualquer coisa que talvez possa ter perdido, ou só dar uma olhada melhor no código se ficar travado.

Lembre-se, autenticação é o ato de fazer login de um usuário e a autorização é o ato de verificar os direitos de acesso de um usuário para interagir com um recurso.

As funções do middleware são usadas como pontes entre partes do código. Quando usados ​​na cadeia de funções de um endpoint, os middleware podem ser incrivelmente úteis na autorização e no tratamento de erros.

Espero que vocês tenham gostado de ler este artigo tanto quanto eu gostei de escrevê-lo. Até a próxima, mantenham-se curiosos e divirtam-se.

Você acha que este tutorial vai ajudar alguém? Não hesite em compartilhar.