Leodario.com

Leodario.com – Tudo sobre Tecnologia

Aula 65 – API JavaScript, Node.js e Express – Refresh Tokens – Renovação segura

Imagem destacada da aula de API

Introdução (3 min)

Olá, futuros arquitetos de sistemas! Bem-vindos à Aula 65, onde desvendaremos um pilar de segurança e usabilidade nas APIs modernas: os Refresh Tokens. Preparem-se para um mergulho profundo e extremamente prático.

Para começarmos, imaginem a seguinte situação. Vocês estão em um parque temático que exige um ingresso para cada atração. Adquirir um novo ingresso para cada brinquedo seria exaustivo e demorado, não é mesmo? Da mesma forma, em uma API, se a cada pequena operação vocês tivessem que refazer todo o processo de autenticação (usuário e senha), a experiência seria péssima e o sistema ficaria vulnerável a ataques de força bruta.

Agora, pensem que vocês têm um ingresso “principal” para entrar no parque (o Access Token), que dura pouco tempo para evitar que, se for roubado, o ladrão se divirta por muito tempo. Para não terem que sair do parque e comprar um novo ingresso principal do lado de fora a cada hora, vocês têm uma pulseira especial (o Refresh Token) que permite ir a um posto de atendimento interno e pegar um novo ingresso principal, sem ter que passar por toda a fila da entrada novamente. Isso é conveniente e, acima de tudo, seguro.

Nesta aula, é exatamente isso que vocês irão construir e compreender: como permitir que usuários permaneçam autenticados por períodos estendidos sem comprometer a segurança da aplicação, tudo isso utilizando Refresh Tokens. Veremos como essa funcionalidade se encaixa de forma harmoniosa no ecossistema Node.js e Express, garantindo que suas APIs sejam robustas e eficientes.

Conceito Fundamental (7 min)

Em um cenário de autenticação baseado em tokens, especialmente com JSON Web Tokens (JWT), é comum utilizarmos dois tipos distintos de tokens para gerenciar o acesso do usuário de forma eficaz e segura: o Access Token e o Refresh Token.

Access Token (Token de Acesso)

    • É o “ingresso principal” que permite o acesso a recursos protegidos na API.
    • Possui uma curta duração (minutos, horas), sendo renovado frequentemente.
    • É enviado em cada requisição para recursos protegidos, geralmente no cabeçalho Authorization como um token
      Bearer

      .

    • Se for interceptado por um atacante, o tempo limitado de vida minimiza o dano potencial, pois ele logo se tornará inválido.

Refresh Token (Token de Renovação)

    • É a “pulseira especial” que possibilita a obtenção de um novo Access Token quando o atual expira.
    • Possui uma longa duração (dias, semanas ou até meses).
    • É enviado ao servidor apenas para a rota de renovação (/refresh), e nunca para acessar recursos protegidos diretamente.
    • Deve ser armazenado de forma extremamente segura no lado do cliente, preferencialmente em um cookie httpOnly, que impede o acesso via JavaScript e mitiga ataques XSS (Cross-Site Scripting).
    • É passível de revogação. Se um Refresh Token for comprometido ou o usuário desejar sair de todos os dispositivos, o servidor pode invalidá-lo imediatamente, bloqueando qualquer tentativa futura de renovação.

O fluxo padrão ocorre da seguinte maneira:

    • O usuário efetua o login (fornecendo usuário e senha).
    • O servidor, após autenticar as credenciais, gera um Access Token (de curta duração) e um Refresh Token (de longa duração).
    • Ambos os tokens são enviados ao cliente. O Access Token é utilizado para as requisições normais e o Refresh Token é armazenado de forma segura.
    • Quando o Access Token expira, o cliente envia o Refresh Token para um endpoint específico do servidor (e.g., /api/refresh-token).
    • O servidor valida o Refresh Token. Se for válido e não estiver revogado, um novo Access Token (e, opcionalmente, um novo Refresh Token para rotação) é gerado e enviado de volta ao cliente.
    • O cliente continua a interagir com a API usando o novo Access Token, sem a necessidade de efetuar login novamente.

Casos de Uso Reais

Essa metodologia é amplamente empregada em diversas aplicações: aplicativos móveis, Single Page Applications (SPAs) como React, Angular e Vue, e aplicações desktop. Ela viabiliza uma experiência de usuário contínua, onde o usuário permanece “logado” por muito tempo, sem comprometer a integridade do sistema.

Vantagens e Desvantagens

Vantagens

    • Segurança Aprimorada: Tokens de acesso de curta duração limitam a janela de oportunidade para ataques, enquanto os refresh tokens, mais seguros, permitem a renovação sem login repetido.
    • Melhor Experiência do Usuário (UX): Reduz a necessidade de autenticações frequentes, contribuindo para uma navegação mais fluida e agradável.
    • Revogação Flexível: Refresh tokens podem ser revogados individualmente (e.g., quando o usuário faz logout de um dispositivo específico), oferecendo controle granular sobre as sessões.

Desvantagens

    • Complexidade Adicional: A arquitetura exige a gestão de dois tipos de tokens e um mecanismo de armazenamento persistente para os refresh tokens (normalmente em um banco de dados).
    • Vetor de Ataque Potencial: Se um refresh token for comprometido, um atacante pode gerar novos access tokens. A revogação e rotação de refresh tokens são estratégias essenciais para mitigar este risco.

Em Node.js e Express, a integração é feita com bibliotecas como jsonwebtoken para a criação e verificação dos JWTs, e middlewares para proteger as rotas. Para o armazenamento persistente dos refresh tokens, usualmente empregamos um banco de dados, como PostgreSQL, MongoDB ou Redis.

Implementação Prática (10 min)

Vamos construir um servidor Express simples que ilustra o fluxo de autenticação com Access Tokens e Refresh Tokens. Para simplificar, armazenaremos os refresh tokens em memória, mas em um ambiente de produção, um banco de dados é essencial.

Setup Inicial

Crie um novo projeto Node.js e instale as dependências:

mkdir refresh-token-api
cd refresh-token-api
npm init -y
npm install express jsonwebtoken dotenv cookie-parser

Crie um arquivo .env na raiz do projeto para suas chaves secretas:

ACCESS_TOKEN_SECRET='sua_chave_secreta_para_access_tokens'
REFRESH_TOKEN_SECRET='sua_chave_secreta_para_refresh_tokens_mais_longa'
PORT=3000

Agora, crie o arquivo principal da sua API, app.js:

// app.js

// 1. Carrega variáveis de ambiente do arquivo .env require('dotenv').config();

// 2. Importa os módulos essenciais const express = require('express'); const jwt = require('jsonwebtoken'); const cookieParser = require('cookie-parser'); // Para lidar com cookies de forma eficiente

// 3. Inicializa a aplicação Express const app = express(); const port = process.env.PORT || 3000; // Define a porta, ou usa 3000 como padrão

// 4. Configurações e Middlewares app.use(express.json()); // Habilita o Express a parsear JSON no corpo das requisições app.use(cookieParser()); // Habilita o uso de cookies

// Simulação de "banco de dados" para refresh tokens (em produção, use um DB real!) // ATENÇÃO: Em ambiente real, esta lista seria persistida em um banco de dados. let refreshTokens = [];

// Chaves secretas para assinar e verificar os tokens JWT const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET; const refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET;

// 5. Middleware de autenticação para proteger rotas function authenticateToken(req, res, next) { // 5.1 Obtém o token de acesso do cabeçalho Authorization const authHeader = req.headers['authorization']; // Formato esperado: "Bearer SEU_ACCESS_TOKEN" const token = authHeader && authHeader.split(' ')[1];

// 5.2 Se não houver token, retorna 401 (Não Autorizado) if (token == null) { console.log('Tentativa de acesso sem token.'); return res.sendStatus(401); }

// 5.3 Verifica o token de acesso jwt.verify(token, accessTokenSecret, (err, user) => { // Se houver erro (token inválido ou expirado), retorna 403 (Proibido) if (err) { console.log('Token de acesso inválido ou expirado:', err.message); return res.sendStatus(403); } // Se o token for válido, armazena as informações do usuário na requisição req.user = user; // 5.4 Continua para a próxima função middleware ou rota next(); }); }

// 6. Rotas da API

// 6.1 Rota de Login: Gera Access Token e Refresh Token app.post('/api/login', (req, res) => { const { username, password } = req.body;

// TODO: Em uma aplicação real, você validaria o username e password contra um DB // Por simplicidade, simulamos um usuário válido. if (!username || !password || username !== 'user' || password !== 'pass') { console.log(Tentativa de login falha para ${username}.); return res.status(400).json({ message: 'Credenciais inválidas.' }); }

// Objeto do usuário (payload do JWT) const user = { name: username };

// 6.1.1 Gera o Access Token (curta duração) const accessToken = jwt.sign(user, accessTokenSecret, { expiresIn: '15m' }); // Expira em 15 minutos

// 6.1.2 Gera o Refresh Token (longa duração) const refreshToken = jwt.sign(user, refreshTokenSecret, { expiresIn: '7d' }); // Expira em 7 dias

// 6.1.3 Armazena o Refresh Token (simulação de DB) refreshTokens.push(refreshToken); console.log(Usuário ${username} logado. Access Token gerado. Refresh Token armazenado.);

// 6.1.4 Envia o Refresh Token como um cookie httpOnly seguro // O Refresh Token não deve ser acessível via JavaScript para maior segurança. res.cookie('refreshToken', refreshToken, { httpOnly: true, // Impede acesso via JavaScript secure: process.env.NODE_ENV === 'production', // Apenas via HTTPS em produção sameSite: 'strict', // Proteção contra CSRF maxAge: 7 24 60 60 1000 // 7 dias em milissegundos });

// 6.1.5 Retorna o Access Token (e possivelmente o refresh token no corpo, mas cookies são melhores) res.json({ accessToken: accessToken, message: 'Login bem-sucedido.' }); });

// 6.2 Rota de Renovação de Token: Usa o Refresh Token para obter um novo Access Token app.post('/api/refresh-token', (req, res) => { // 6.2.1 Obtém o Refresh Token do cookie const refreshToken = req.cookies.refreshToken;

// 6.2.2 Se não houver Refresh Token, retorna 401 if (!refreshToken) { console.log('Tentativa de renovação sem Refresh Token.'); return res.status(401).json({ message: 'Refresh Token não fornecido.' }); }

// 6.2.3 Verifica se o Refresh Token está na nossa "lista" (simulação de DB) if (!refreshTokens.includes(refreshToken)) { console.log('Refresh Token inválido ou não encontrado na lista.'); return res.status(403).json({ message: 'Refresh Token inválido.' }); }

// 6.2.4 Verifica a validade do Refresh Token jwt.verify(refreshToken, refreshTokenSecret, (err, user) => { if (err) { console.log('Refresh Token expirado ou inválido:', err.message); // Se o Refresh Token for inválido, remova-o da lista (se existir) refreshTokens = refreshTokens.filter(token => token !== refreshToken); // Limpa o cookie do cliente res.clearCookie('refreshToken'); return res.status(403).json({ message: 'Refresh Token expirado ou inválido. Faça login novamente.' }); }

// 6.2.5 Se válido, gera um novo Access Token const newAccessToken = jwt.sign({ name: user.name }, accessTokenSecret, { expiresIn: '15m' }); console.log(Novo Access Token gerado para ${user.name}.);

// OPCIONAL: Rotação de Refresh Tokens (gera um novo Refresh Token e invalida o antigo) // const newRefreshToken = jwt.sign({ name: user.name }, refreshTokenSecret, { expiresIn: '7d' }); // refreshTokens = refreshTokens.filter(token => token !== refreshToken); // Remove o antigo // refreshTokens.push(newRefreshToken); // Adiciona o novo // res.cookie('refreshToken', newRefreshToken, { // httpOnly: true, // secure: process.env.NODE_ENV === 'production', // sameSite: 'strict', // maxAge: 7 24 60 60 1000 // });

res.json({ accessToken: newAccessToken, message: 'Token de acesso renovado com sucesso.' }); }); });

// 6.3 Rota de Logout: Revoga o Refresh Token app.post('/api/logout', (req, res) => { const refreshToken = req.cookies.refreshToken;

if (!refreshToken) { console.log('Tentativa de logout sem Refresh Token no cookie.'); return res.status(204).json({ message: 'Nenhum Refresh Token para remover.' }); // 204 No Content }

// Remove o Refresh Token da nossa "lista" (revoga) refreshTokens = refreshTokens.filter(token => token !== refreshToken); console.log('Refresh Token removido da lista (revogado).');

// Limpa o cookie do navegador res.clearCookie('refreshToken', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict' }); console.log('Cookie de Refresh Token limpo.'); res.status(204).json({ message: 'Logout bem-sucedido.' }); });

// 6.4 Rota Protegida: Apenas usuários com Access Token válido podem acessar app.get('/api/protected-data', authenticateToken, (req, res) => { // req.user contém as informações do usuário do Access Token console.log(Acesso à rota protegida por ${req.user.name}.); res.json({ message: Bem-vindo, ${req.user.name}! Você acessou dados protegidos., data: 'Informações confidenciais aqui.' }); });

// 7. Inicializa o servidor app.listen(port, () => { console.log(Servidor rodando em http://localhost:${port}); console.log('Para testar:'); console.log('1. Fazer login: curl -X POST -H "Content-Type: application/json" -d \'{"username":"user","password":"pass"}\' http://localhost:3000/api/login'); console.log('2. Acessar rota protegida (com o token do passo 1): curl -H "Authorization: Bearer SEU_ACCESS_TOKEN" http://localhost:3000/api/protected-data'); console.log('3. Renovar token: curl -X POST http://localhost:3000/api/refresh-token --cookie "refreshToken=SEU_REFRESH_TOKEN_DO_COOKIE"'); console.log('4. Fazer logout: curl -X POST http://localhost:3000/api/logout --cookie "refreshToken=SEU_REFRESH_TOKEN_DO_COOKIE"'); });

Configurações para HostGator Plano M

Para implantar em um ambiente como o HostGator Plano M, é vital considerar alguns pontos:

    • Variáveis de Ambiente (.env): O HostGator geralmente permite configurar variáveis de ambiente diretamente no cPanel ou via um arquivo de configuração de inicialização. Evite deixar chaves secretas diretamente no código-fonte.
    • Porta (process.env.PORT): Em hosts compartilhados, sua aplicação Node.js pode não rodar diretamente na porta 80/443. Um servidor proxy (como Nginx ou Apache no HostGator) geralmente encaminha requisições da porta padrão para a porta interna da sua aplicação Node. Certifique-se de que sua aplicação escute na porta fornecida pelo ambiente, ou em uma porta padrão como 3000, e que o proxy esteja configurado corretamente para encaminhar.
    • Armazenamento Persistente: Em produção, o array refreshTokens em memória não funcionará, pois será resetado a cada reinício do servidor. Use um banco de dados real (MySQL/PostgreSQL, ou MongoDB se o plano suportar) para armazenar os refresh tokens.
    • HTTPS (secure: true): Configure secure: true para seus cookies em produção. No HostGator, isso significa que você precisará ter um certificado SSL/TLS ativo.
    • Logging: Em vez de apenas console.log, considere bibliotecas de logging mais robustas como Winston ou Pino para gerenciar logs em arquivos, facilitando a depuração em um ambiente de servidor.

Error Handling Eficiente

No código, utilizamos blocos if e return res.status(XYZ).json(...) para lidar com erros como tokens inválidos ou ausentes. Isso assegura que a API retorne respostas claras e códigos de status HTTP apropriados, um pilar das melhores práticas.

Testes Básicos (usando curl)

Você pode testar o fluxo com os comandos curl que já estão documentados no final do arquivo app.js:

# 1. Login (obtém Access Token no corpo e Refresh Token no cookie)
curl -X POST -H "Content-Type: application/json" -d '{"username":"user","password":"pass"}' http://localhost:3000/api/login -c cookie-jar.txt -D headers.txt

Do arquivo 'headers.txt', você vai precisar do Access Token (Bearer)

📚 Informações da Aula

Curso: API Completo - Node.js & Express

Tempo estimado: 25 minutos

Pré-requisitos: JavaScript básico

Do cookie-jar.txt, você vai precisar do Refresh Token (ele será enviado automaticamente nos próximos curls com -b cookie-jar.txt)

Exemplo de Access Token obtido: "eyJhbGciOiJIUzI1Ni..."

2. Acessar Rota Protegida (substitua SEU_ACCESS_TOKEN)

curl -H "Authorization: Bearer SEU_ACCESS_TOKEN" http://localhost:3000/api/protected-data -b cookie-jar.txt

3. Renovar Token (o refresh token será enviado automaticamente do cookie-jar)

curl -X POST http://localhost:3000/api/refresh-token -b cookie-jar.txt -c cookie-jar.txt

O comando acima retornará um NOVO Access Token. Você pode usá-lo para acessar a rota protegida novamente.

4. Fazer Logout (o refresh token será enviado automaticamente do cookie-jar e revogado)

curl -X POST http://localhost:3000/api/logout -b cookie-jar.txt -c cookie-jar.txt

Para os comandos curl, a flag -c cookie-jar.txt salva os cookies recebidos em um arquivo, e -b cookie-jar.txt os envia em requisições futuras, simulando o comportamento de um navegador. Use -D headers.txt para ver os cabeçalhos da resposta, onde o Set-Cookie para o refresh token estará visível.

Exercício Hands-On (5 min)

Para solidificar seu aprendizado, proponho um desafio prático:

Desafio

Modifique a implementação atual para que os refresh tokens não sejam armazenados em um array em memória, mas sim em um arquivo JSON. Isso simulará um armazenamento persistente e permitirá que os refresh tokens sobrevivam a reinícios do servidor, como aconteceria com um banco de dados real. Além disso, certifique-se de que a rota /api/logout remova o refresh token do arquivo JSON.

Solução Detalhada Passo a Passo

    • Crie o arquivo de armazenamento:

      Crie um arquivo chamado refreshTokens.json na raiz do seu projeto. Inicialmente, ele pode conter um array vazio:

      // refreshTokens.json
      []
      

    • Adicione funções para ler e escrever no arquivo JSON:

      No app.js, adicione as seguintes funções. Elas serão responsáveis por interagir com seu “banco de dados” de refresh tokens.

      // ... no início do app.js, após os imports ...
      const fs = require('fs'); // Módulo nativo do Node.js para manipulação de arquivos

      const REFRESH_TOKENS_FILE = 'refreshTokens.json';

      // Função para ler os refresh tokens do arquivo function readRefreshTokens() { try { const data = fs.readFileSync(REFRESH_TOKENS_FILE, 'utf8'); return JSON.parse(data); } catch (error) { // Se o arquivo não existir ou for inválido, retorna um array vazio console.error('Erro ao ler refreshTokens.json, criando um novo arquivo ou array vazio.', error.message); return []; } }

      // Função para escrever os refresh tokens no arquivo function writeRefreshTokens(tokens) { try { fs.writeFileSync(REFRESH_TOKENS_FILE, JSON.stringify(tokens, null, 2), 'utf8'); } catch (error) { console.error('Erro ao escrever em refreshTokens.json.', error.message); } }

      // Inicializa o array de refresh tokens lendo do arquivo let refreshTokens = readRefreshTokens();

      // ... o restante do seu código ...

    • Modifique as rotas login e refresh-token para usar as novas funções:
      • Na rota /api/login, ao gerar um novo refresh token:
        // Antigo: refreshTokens.push(refreshToken);
        // Novo:
        refreshTokens.push(refreshToken);
        writeRefreshTokens(refreshTokens); // Salva no arquivo
        console.log(Usuário ${username} logado. Access Token gerado. Refresh Token armazenado e persistido.);
        

      • Na rota /api/refresh-token, ao revogar um token inválido e ao gerenciar a rotação (se implementado):
        // Quando o Refresh Token for inválido/expirado:
        // Antigo: refreshTokens = refreshTokens.filter(token => token !== refreshToken);
        // Novo:
        refreshTokens = refreshTokens.filter(token => token !== refreshToken);
        writeRefreshTokens(refreshTokens); // Salva a lista atualizada
        // ...

        // Se você implementar a rotação de refresh tokens: // refreshTokens = refreshTokens.filter(token => token !== refreshToken); // refreshTokens.push(newRefreshToken); // writeRefreshTokens(refreshTokens); // Salva a lista atualizada com o novo token

    • Modifique a rota logout para remover o token do arquivo:
      // Na rota /api/logout:
      // Antigo: refreshTokens = refreshTokens.filter(token => token !== refreshToken);
      // Novo:
      refreshTokens = refreshTokens.filter(token => token !== refreshToken);
      writeRefreshTokens(refreshTokens); // Salva a lista atualizada sem o token revogado
      console.log('Refresh Token removido da lista (revogado) e persistido.');
      // ...
      

Como Testar e Validar o Resultado

Execute o servidor (node app.js). Utilize os comandos curl da seção de implementação prática para testar o login, acesso protegido, renovação e logout. Após cada operação que modifica os refresh tokens (login, logout, refresh de um token inválido), verifique o conteúdo do arquivo refreshTokens.json para garantir que ele esteja sendo atualizado corretamente.

Um teste crucial será: faça login, reinicie o servidor Node.js, e tente renovar o token. Se o Refresh Token ainda for válido e estiver no arquivo JSON, ele deverá funcionar, provando que o armazenamento está persistente.

Troubleshooting dos Erros Mais Comuns

    • ENOENT: no such file or directory, open 'refreshTokens.json' (Erro ao ler/escrever): Significa que o arquivo refreshTokens.json não existe ou o caminho está incorreto. Crie o arquivo na raiz do projeto.
    • SyntaxError: Unexpected token o in JSON at position 1 (Erro no JSON.parse): Indica que o arquivo refreshTokens.json não contém JSON válido. Verifique se ele está no formato correto (e.g., []).
    • Token inválido/expirado após reiniciar o servidor: Se isso acontecer após implementar o armazenamento em arquivo, verifique se as funções readRefreshTokens e writeRefreshTokens estão sendo chamadas nos locais corretos e se estão realmente salvando e carregando os dados.

Próximos Passos Sugeridos

Para levar sua API ao próximo patamar:

    • Integração com um Banco de Dados Real: Substitua o arquivo JSON por um banco de dados como PostgreSQL (com pg), MongoDB (com mongoose) ou Redis (com ioredis) para gerenciar os refresh tokens de forma verdadeiramente escalável e robusta.
    • Rotação de Refresh Tokens: Implemente a lógica de rotação, onde a cada renovação, um novo Refresh Token é gerado e o antigo é invalidado. Isso aumenta a segurança ao limitar ainda mais a utilidade de um token comprometido.
    • Blacklisting de Access Tokens: Para revogação imediata de Access Tokens (e.g., em um logout), use uma “blacklist” (geralmente em memória ou em Redis) para marcar Access Tokens como inválidos antes de sua expiração natural.
    • Auditoria e Detecção de Reuso: Implemente mecanismos para detectar tentativas de reuso de Refresh Tokens que já deveriam ter sido invalidados, indicando uma possível tentativa de ataque.

🚀 Pronto para a próxima aula?

Continue sua jornada no desenvolvimento de APIs e domine Node.js & Express!

📚 Ver todas as aulas