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:

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:

Agora, se realizarmos a autenticação usando 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):

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.

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!