Leodario.com

Leodario.com – Tudo sobre Tecnologia

Aula 64 – API JavaScript, Node.js e Express – JWT Implementation – Prática completa

Imagem destacada da aula de API

Olá, futuros arquitetos de APIs! Sejam bem-vindos à Aula 64: JWT Implementation – Prática completa, uma jornada avançada para dominar a espinha dorsal da segurança em APIs modernas. Eu, seu professor PHD e especialista mundial em APIs, estou aqui para guiar vocês através de cada detalhe, garantindo que saiam desta aula com um conhecimento prático e profundo.

Introdução (3 min)

Imagine que você está em um concerto disputado. Ao invés de checar seu nome em uma lista gigante na entrada toda vez que você sai para pegar uma água ou usar o banheiro, você recebe um ingresso digital único. Este ingresso contém todas as informações necessárias para provar que você comprou seu lugar (seu nome, setor, data), e ele é assinado digitalmente pelo organizador do evento para garantir que ninguém possa falsificá-lo. Ao reentrar, o segurança apenas confere a validade e a assinatura do seu ingresso, sem precisar ir ao balcão de vendas.

Essa é a essência do JSON Web Token (JWT) no universo das APIs. É um passaporte digital seguro e autossuficiente que prova a identidade de um usuário sem a necessidade de o servidor consultar um banco de dados a cada requisição. Isso é essencial para APIs modernas, pois viabiliza a autenticação e autorização de forma escalável, eficiente e stateless (sem estado), um pilar fundamental para microsserviços e aplicações distribuídas.

Nesta aula, vocês não apenas compreenderão o “o quê” e o “porquê”, mas mergulharão no “como”. Aprenderão a implementar um sistema completo de autenticação JWT do zero em Node.js com Express, desde a criação do token até a proteção de rotas, incluindo as melhores práticas de mercado e considerações para ambientes de produção como o HostGator Plano M. Nosso foco será a criação de código funcional e robusto, pronto para ser adaptado aos seus projetos.

Conceito Fundamental (7 min)

O JSON Web Token (JWT) é um padrão aberto (RFC 7519) que define uma forma compacta e segura para transmitir informações entre partes como um objeto JSON. Essas informações podem ser verificadas e confiadas porque são assinadas digitalmente.

Um JWT é composto por três partes, separadas por pontos (.):

    • Header (Cabeçalho): Geralmente contém dois campos: o tipo do token, que é JWT, e o algoritmo de assinatura usado, como HS256 ou RS256.
    • Payload (Carga Útil): Contém as claims (declarações). As claims são declarações sobre uma entidade (geralmente o usuário) e metadados adicionais. Existem três tipos de claims:
      • Registered Claims: Conjunto de claims pré-definidas (por exemplo, iss para emissor, exp para tempo de expiração, sub para assunto/usuário).
      • Public Claims: Podem ser definidas à vontade por quem usa JWT, mas devem ser registradas na IANA ou ser definidas como um URI contendo um namespace resistente a colisões.
      • Private Claims: São criadas para concordar entre as partes que estão usando um JWT, sem serem registradas ou públicas.
    • Signature (Assinatura): Criada usando o cabeçalho e a carga útil codificados em Base64Url, mais um segredo (secret) para algoritmos simétricos (como HS256) ou uma chave privada para algoritmos assimétricos (como RS256). A assinatura é utilizada para verificar a integridade do token e garantir que ele não foi alterado no caminho.

A estrutura de um JWT se assemelha a AAAAA.BBBBB.CCCCC, onde AAAA é o header codificado, BBBB é o payload codificado, e CCCC é a assinatura. Essa natureza autossuficiente do token facilita a autenticação stateless, um dos seus maiores benefícios.

Casos de Uso e Integração:

    • Autenticação de Usuários: É o uso mais prevalente. Após um login bem-sucedido, o servidor emite um JWT para o cliente. O cliente então anexa este token nas requisições subsequentes para acessar recursos protegidos.
    • Autorização: O token pode carregar informações sobre os papéis ou permissões do usuário, agilizando a verificação de autorização em diferentes microsserviços.
    • Single Sign-On (SSO):Viabiliza a autenticação em múltiplos serviços com uma única credencial.
    • Comunicação entre Microsserviços:Garante a identidade e a integridade das requisições entre serviços internos.

Vantagens e Desvantagens:

    • Vantagens:
      • Stateless: Não há necessidade de armazenar informações de sessão no servidor, reduzindo a carga e otimizando a escalabilidade.
      • Portabilidade: Pode ser usado em vários domínios e sistemas, tornando-o altamente flexível.
      • Eficiência: O token contém todas as informações necessárias, diminuindo a necessidade de consultas adicionais ao banco de dados.
      • Segurança: Assinatura digital protege contra adulterações.
    • Desvantagens:
      • Tamanho: Tokens podem ser maiores que IDs de sessão, aumentando o tráfego em requisições.
      • Revogação: Revogar um JWT antes de sua expiração é complexo, pois ele é autossuficiente. Exige mecanismos como “blacklists” ou “short-lived tokens” combinados com “refresh tokens”.
      • Vulnerabilidade a XSS: Se armazenado em localStorage, é vulnerável a ataques de Cross-Site Scripting (XSS). O armazenamento em HttpOnly cookies mitiga isso, mas apresenta outras complexidades (CSRF).
      • Informações Sensíveis: O payload é apenas codificado, não criptografado. Nunca coloque informações sensíveis diretamente no payload do JWT.

Implementação Prática (10 min)

Vamos agora construir um sistema de autenticação JWT completo com Node.js e Express. Precisaremos de algumas dependências:

npm init -y
npm install express jsonwebtoken bcryptjs dotenv

    • express: Para criar o servidor web.
    • jsonwebtoken: Para gerar e verificar JWTs.
    • bcryptjs: Para hashear senhas de forma segura.
    • dotenv: Para carregar variáveis de ambiente (essencial para o JWT_SECRET).

Crie um arquivo .env na raiz do seu projeto para suas variáveis de ambiente. No HostGator Plano M, essas variáveis são configuradas diretamente no painel de controle ou via .htaccess se você estiver usando Phusion Passenger ou um setup similar para Node.js, mas para fins de desenvolvimento local, .env é prático. O importante é que a chave secreta NUNCA seja hardcoded.

JWT_SECRET="sua_chave_secreta_muito_forte_aqui_para_assinatura_jwt"
PORT=3000

Agora, vamos aos arquivos:

server.js (Ponto de entrada da aplicação)

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

const express = require('express'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const app = express();

// Middleware para parsear JSON no corpo das requisições app.use(express.json());

// --- Simulando um banco de dados de usuários --- // Em uma aplicação real, estes usuários viriam de um banco de dados (MongoDB, PostgreSQL, etc.) // As senhas deveriam ser sempre hasheadas. const users = []; // Array vazio para simular o armazenamento de usuários

// Função para gerar um token JWT const generateAccessToken = (user) => { // Aqui definimos o payload do token. CUIDADO para não incluir informações sensíveis. // O ideal é incluir apenas o ID do usuário e, talvez, seus papéis de autorização. console.log([JWT] Gerando token para usuário: ${user.username}); return jwt.sign( { id: user.id, username: user.username }, // Payload (carga útil) process.env.JWT_SECRET, // Chave secreta para assinatura { expiresIn: '1h' } // O token expira em 1 hora ); };

// --- Rota de Registro de Usuário --- app.post('/register', async (req, res) => { try { const { username, password } = req.body;

// Validação básica de entrada if (!username || !password) { console.warn('[Validation] Tentativa de registro com dados incompletos.'); return res.status(400).json({ message: 'Nome de usuário e senha são obrigatórios.' }); }

// Verifica se o usuário já existe if (users.find(u => u.username === username)) { console.warn([Register] Tentativa de registro de usuário existente: ${username}); return res.status(409).json({ message: 'Nome de usuário já existe.' }); // 409 Conflict }

// Hash da senha antes de armazenar const hashedPassword = await bcrypt.hash(password, 10); // O '10' é o saltRounds, complexidade do hash

const newUser = { id: users.length + 1, // Simula um ID incremental username, password: hashedPassword }; users.push(newUser); console.log([Register] Novo usuário registrado: ${username} (ID: ${newUser.id})); res.status(201).json({ message: 'Usuário registrado com sucesso!', userId: newUser.id }); } catch (error) { console.error('[Error] Erro no registro de usuário:', error.message); res.status(500).json({ message: 'Erro interno do servidor durante o registro.' }); } });

// --- Rota de Login de Usuário --- app.post('/login', async (req, res) => { try { const { username, password } = req.body;

// Validação básica if (!username || !password) { console.warn('[Validation] Tentativa de login com dados incompletos.'); return res.status(400).json({ message: 'Nome de usuário e senha são obrigatórios.' }); }

// Encontra o usuário (simulado) const user = users.find(u => u.username === username); if (!user) { console.warn([Login] Tentativa de login falha para usuário inexistente: ${username}); return res.status(401).json({ message: 'Credenciais inválidas.' }); // 401 Unauthorized }

// Compara a senha fornecida com a senha hasheada armazenada const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { console.warn([Login] Tentativa de login falha (senha incorreta) para usuário: ${username}); return res.status(401).json({ message: 'Credenciais inválidas.' }); }

// Se as credenciais estiverem corretas, gera um JWT const accessToken = generateAccessToken(user); console.log([Login] Usuário ${username} logado com sucesso. Token emitido.); res.json({ accessToken }); } catch (error) { console.error('[Error] Erro no login de usuário:', error.message); res.status(500).json({ message: 'Erro interno do servidor durante o login.' }); } });

// --- Middleware de Autenticação JWT --- // Este middleware será usado para proteger rotas. Ele verifica a validade do token. const authenticateToken = (req, res, next) => { const authHeader = req.headers['authorization']; // O token geralmente vem no formato 'Bearer SEU_TOKEN_AQUI' const token = authHeader && authHeader.split(' ')[1];

if (token == null) { console.warn('[Auth] Acesso negado: token não fornecido.'); return res.status(401).json({ message: 'Acesso negado: Token não fornecido.' }); // 401 Unauthorized }

jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) { // Se o token for inválido ou expirado console.warn('[Auth] Token inválido ou expirado:', err.message); return res.status(403).json({ message: 'Token inválido ou expirado.' }); // 403 Forbidden } // Se o token for válido, o payload decodificado é anexado ao objeto request req.user = user; console.log([Auth] Token validado para usuário: ${user.username}); next(); // Continua para a próxima função middleware ou rota }); };

// --- Rota Protegida (requer JWT válido) --- app.get('/protected', authenticateToken, (req, res) => { console.log([Access] Usuário ${req.user.username} acessou rota protegida.); res.json({ message: 'Você acessou uma rota protegida!', user: req.user, data: 'Informações confidenciais aqui.' }); });

// --- Início do Servidor --- const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(Servidor rodando na porta ${PORT}); console.log(Chave JWT utilizada (primeiros 5 caracteres): ${process.env.JWT_SECRET.substring(0, 5)}...); console.log('Instruções de teste:'); console.log('1. REGISTRAR: POST /register com {"username": "testuser", "password": "password123"}'); console.log('2. LOGIN: POST /login com {"username": "testuser", "password": "password123"} para obter o token.'); console.log('3. ACESSAR PROTEGIDO: GET /protected com o token no header "Authorization: Bearer SEU_TOKEN".'); });

Melhores Práticas Enterprise e HostGator Plano M:

    • Variáveis de Ambiente: Sempre use process.env para chaves secretas (JWT_SECRET) e outras configurações sensíveis. No HostGator, configure-as no painel de controle (geralmente em “Setup Node.js App”) ou via .htaccess se utilizando Phusion Passenger com um arquivo passenger_wsgi.py ou similar para mapear o Node.js.
    • Força da Senha: Implemente políticas de senhas fortes (mínimo de caracteres, letras maiúsculas/minúsculas, números, símbolos).
    • Salt Rounds do Bcrypt: O valor 10 para saltRounds é um bom ponto de partida. Ajuste conforme o poder computacional do seu servidor; valores maiores são mais seguros, mas mais lentos.
    • Expiração do Token (expiresIn): Use tokens de curta duração (e.g., 15 minutos a 1 hora) para o token de acesso. Combine-os com Refresh Tokens (não implementado aqui para manter o foco, mas essencial em produção) para uma melhor experiência do usuário e segurança na revogação.
    • Logging: Usar console.log para demos é aceitável, mas em produção, substitua por uma biblioteca de logging robusta como Winston ou Pino. Isso possibilita logs estruturados, níveis de log (info, warn, error) e rotação de logs.
    • Validação de Entrada: O exemplo usa validação básica. Em produção, use bibliotecas como Joi ou Express-Validator para uma validação completa e robusta de todos os dados de entrada.
    • Error Handling: Implemente um middleware de tratamento de erros centralizado para capturar e gerenciar erros de forma consistente em toda a aplicação.
    • HTTPS: Sempre implemente HTTPS em produção para proteger o token em trânsito. O HostGator oferece certificados SSL.
    • CORS: Para APIs consumidas por front-ends em domínios diferentes, configure CORS adequadamente para controlar quais origens podem acessar sua API.

Testes Básicos (com curl ou Postman/Insomnia):

Para testar, inicie o servidor com:

node server.js

    • Registrar um Usuário (POST):
      curl -X POST -H "Content-Type: application/json" -d '{"username": "devuser", "password": "securepassword"}' http://localhost:3000/register
      

      Espere uma resposta como: {"message":"Usuário registrado com sucesso!","userId":1}

    • Fazer Login (POST):
      curl -X POST -H "Content-Type: application/json" -d '{"username": "devuser", "password": "securepassword"}' http://localhost:3000/login
      

      Copie o accessToken retornado. Será algo como: {"accessToken":"eyJhbGciOiJIUzI1Ni..."}

    • Acessar Rota Protegida (GET, sem token):
      curl http://localhost:3000/protected
      

      Espere: {"message":"Acesso negado: Token não fornecido."} (Status 401)

    • Acessar Rota Protegida (GET, com token):

      Substitua SEU_TOKEN_AQUI pelo token que você copiou no passo 2.

      curl -H "Authorization: Bearer SEU_TOKEN_AQUI" http://localhost:3000/protected
      

      Espere: {"message":"Você acessou uma rota protegida!","user":{"id":1,"username":"devuser"},"data":"Informações confidenciais aqui."} (Status 200)

Exercício Hands-On (5 min)

Desafio Prático:

Seu desafio agora é expandir a funcionalidade de autenticação. Crie uma nova rota protegida chamada /admin que só possa ser acessada por usuários que possuam um campo role: 'admin' no seu payload JWT. Modifique o processo de registro para permitir que um usuário seja registrado como admin ou user, e certifique-se de que o role seja incluído no JWT.

Solução Detalhada Passo a Passo:

    • Modificar generateAccessToken para incluir o role:

      Altere a função para aceitar um role no payload.

      const generateAccessToken = (user) => {
          // Agora o payload inclui o 'role' do usuário
          return jwt.sign(
              { id: user.id, username: user.username, role: user.role }, 
              process.env.JWT_SECRET, 
              { expiresIn: '1h' }
          );
      };
      

    • Modificar users e register para incluir o role:

      Adicione um campo role ao objeto newUser no register e ao usuário simulado. Pode ser um role padrão ou um que venha na requisição (com validação!)

      Vamos simplificar para o exercício, adicionando um role padrão e permitindo que um usuário específico seja admin.

      // No server.js, dentro da rota POST /register
      app.post('/register', async (req, res) => {
          try {
              const { username, password, role } = req.body; // Agora esperando 'role' também

      if (!username || !password) { return res.status(400).json({ message: 'Nome de usuário e senha são obrigatórios.' }); } if (users.find(u => u.username === username)) { return res.status(409).json({ message: 'Nome de usuário já existe.' }); }

      const hashedPassword = await bcrypt.hash(password, 10);

      // Define o papel do usuário. Por segurança, não permitir que o cliente defina 'admin' diretamente // a menos que haja um processo de autorização para isso. // Para este exercício, vamos permitir que seja passado no corpo para testar. // Em um cenário real, você teria um campo padrão 'user' e um admin seria configurado manualmente. const userRole = role === 'admin' ? 'admin' : 'user';

      const newUser = { id: users.length + 1, username, password: hashedPassword, role: userRole // Adicionando o papel do usuário }; users.push(newUser); console.log([Register] Novo usuário registrado: ${username} (ID: ${newUser.id}, Role: ${newUser.role})); res.status(201).json({ message: 'Usuário registrado com sucesso!', userId: newUser.id, role: newUser.role }); } catch (error) { console.error('[Error] Erro no registro de usuário:', error.message); res.status(500).json({ message: 'Erro interno do servidor durante o registro.' }); } });

    • Criar um novo middleware para admin (authorizeRole):

      Este middleware verificará se o req.user.role (definido pelo authenticateToken) corresponde ao requiredRole.

      // Novo middleware para verificar papéis de usuário
      const authorizeRole = (requiredRole) => {
          return (req, res, next) => {
              // req.user já foi populado pelo authenticateToken
              if (req.user && req.user.role === requiredRole) {
                  console.log([Authorization] Usuário ${req.user.username} tem o papel "${requiredRole}".);
                  next(); // O usuário tem o papel necessário, pode prosseguir
              } else {
                  console.warn([Authorization] Acesso proibido para ${req.user ? req.user.username : 'usuário não autenticado'}. Papel "${requiredRole}" requerido.);
                  res.status(403).json({ message: 'Acesso proibido: Você não tem permissão para acessar este recurso.' }); // 403 Forbidden
              }
          };
      };
      

    • Criar a rota /admin e aplicar os middlewares:
      // --- Rota Protegida para Admin (requer JWT válido e role 'admin') ---
      app.get('/admin', authenticateToken, authorizeRole('admin'), (req, res) => {
          console.log([Access] Administrador ${req.user.username} acessou rota de admin.);
          res.json({
              message: 'Bem-vindo, Administrador!',
              user: req.user,
              data: 'Conteúdo exclusivo para administradores.'
          });
      });
      

Como Testar e Validar o Resultado:

    • Reinicie o servidor Node.js.
    • Registre um usuário normal:
      curl -X POST -H "Content-Type: application/json" -d '{"username": "user", "password": "password123", "role": "user"}' http://localhost:3000/register
      

    • Registre um usuário admin:
      curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "adminpassword", "role": "admin"}' http://localhost:3000/register
      

    • Faça login como o usuário normal e pegue o token:
      curl -X POST -H "Content-Type: application/json" -d '{"username": "user", "password": "password123"}' http://localhost:3000/login
      

      Tente acessar /admin com este token. Espere um 403 Forbidden.

    • Faça login como o usuário admin e pegue o token:
      curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "adminpassword"}' http://localhost:3000/login
      

      Tente acessar /admin com este token. Espere um 200 OK.

    • Tente acessar /protected com ambos os tokens. Ambos devem funcionar.

Troubleshooting dos Erros Mais Comuns:

    • 401 Unauthorized ou Token não fornecido: Você esqueceu de incluir o cabeçalho Authorization: Bearer SEU_TOKEN ou o token está mal formatado.
    • 403 Forbidden ou Token inválido ou expirado: O token expirou, a chave secreta usada para assinar/verificar está incorreta, ou o token foi adulterado. Verifique também se a rota /admin retornou 403 por falta de permissão, não por token inválido.
    • 403 Forbidden (na rota /admin para usuário normal): Comportamento esperado! O middleware authorizeRole('admin') funcionou corretamente e negou o acesso.
    • 500 Internal Server Error: Verifique os logs do servidor no seu terminal. Pode ser um erro de código, como acesso a uma propriedade indefinida ou um problema com o bcrypt ou jsonwebtoken.
    • Variáveis de Ambiente (JWT_SECRET undefined): Certifique-se de que seu arquivo .env está na raiz do projeto e que require('dotenv').config() está no topo do seu server.js. Em produção no HostGator, garanta que as variáveis foram definidas no painel.

Próximos Passos Sugeridos:

    • Implementar Refresh Tokens: Para uma experiência de usuário aprimorada e maior segurança na revogação de tokens.
    • Persistência de Usuários: Conecte sua aplicação a um banco de dados real (MongoDB com Mongoose, PostgreSQL com Sequelize, etc.) em vez do array em memória.
    • Testes Unitários e de Integração: Escreva testes para suas rotas e middlewares de autenticação/autorização para garantir a confiabilidade do seu código.
    • Rate Limiting: Implemente proteção contra ataques de força bruta nas rotas de login/registro.
    • CORS e Segurança: Adicione middlewares como helmet para segurança adicional e configure CORS adequadamente.

Parabéns! Você acaba de construir uma base sólida para a segurança de suas APIs utilizando JWT. A complexidade do mundo real é vasta, mas as ferramentas e o conhecimento que você adquiriu hoje o preparam de forma excelente para qualquer desafio. Continue explorando e desenvolvendo!

🚀 Pronto para a próxima aula?

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

📚 Ver todas as aulas