Autenticação com JWT no Node.js

1. Fundamentos do JWT

JWT (JSON Web Token) é um padrão aberto (RFC 7519) que define uma forma compacta e autossuficiente de transmitir informações entre partes como um objeto JSON. Um token JWT é composto por três partes separadas por pontos:

  • Header: contém o tipo do token e o algoritmo de assinatura (ex: {"alg": "HS256", "typ": "JWT"})
  • Payload: contém as claims (declarações) como dados do usuário e metadados (expiração, emissor)
  • Signature: assinatura gerada a partir do header, payload e uma chave secreta

Diferente de sessões tradicionais (que exigem armazenamento no servidor), o JWT permite autenticação stateless: o servidor não precisa manter estado da sessão, pois toda informação necessária está contida no token. Tokens de acesso (access tokens) têm curta duração (ex: 15 minutos), enquanto refresh tokens (mais longos) permitem renovar o access token sem exigir novo login.

2. Configuração do Ambiente Node.js

Crie um novo projeto e instale as dependências:

npm init -y
npm install express jsonwebtoken bcryptjs dotenv
npm install prisma @prisma/client --save

Estrutura de pastas sugerida:

src/
├── controllers/
│   └── authController.js
├── middleware/
│   └── authMiddleware.js
├── models/
│   └── User.js
├── routes/
│   └── authRoutes.js
└── server.js

Crie um arquivo .env na raiz:

JWT_SECRET=seu_segredo_super_seguro_aqui
JWT_EXPIRES_IN=15m
REFRESH_TOKEN_SECRET=outro_segredo_para_refresh

3. Criação do Modelo de Usuário e Banco de Dados

Usando Prisma (ORM moderno), defina o schema em prisma/schema.prisma:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite" // ou postgresql
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  password  String
  createdAt DateTime @default(now())
}

Execute npx prisma migrate dev --name init para criar o banco.

Função para hash de senha no registro:

const bcrypt = require('bcryptjs');

async function hashPassword(password) {
  const salt = await bcrypt.genSalt(12);
  return bcrypt.hash(password, salt);
}

4. Implementação do Registro e Login

Controller de autenticação (src/controllers/authController.js):

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

exports.register = async (req, res) => {
  try {
    const { email, password, name } = req.body;

    // Validações básicas
    if (!email || !password || !name) {
      return res.status(400).json({ error: 'Todos os campos são obrigatórios' });
    }

    // Verificar se email já existe
    const userExists = await prisma.user.findUnique({ where: { email } });
    if (userExists) {
      return res.status(409).json({ error: 'Email já cadastrado' });
    }

    // Hash da senha
    const hashedPassword = await bcrypt.hash(password, 12);

    // Criar usuário
    const user = await prisma.user.create({
      data: { email, password: hashedPassword, name }
    });

    // Gerar token JWT
    const token = jwt.sign(
      { userId: user.id, email: user.email },
      process.env.JWT_SECRET,
      { expiresIn: process.env.JWT_EXPIRES_IN }
    );

    res.status(201).json({ token, user: { id: user.id, email: user.email, name: user.name } });
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Erro interno do servidor' });
  }
};

exports.login = async (req, res) => {
  try {
    const { email, password } = req.body;

    // Buscar usuário
    const user = await prisma.user.findUnique({ where: { email } });
    if (!user) {
      return res.status(401).json({ error: 'Credenciais inválidas' });
    }

    // Verificar senha
    const isValidPassword = await bcrypt.compare(password, user.password);
    if (!isValidPassword) {
      return res.status(401).json({ error: 'Credenciais inválidas' });
    }

    // Gerar token
    const token = jwt.sign(
      { userId: user.id, email: user.email },
      process.env.JWT_SECRET,
      { expiresIn: process.env.JWT_EXPIRES_IN }
    );

    res.json({ token, user: { id: user.id, email: user.email, name: user.name } });
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Erro interno do servidor' });
  }
};

Rotas (src/routes/authRoutes.js):

const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');

router.post('/register', authController.register);
router.post('/login', authController.login);

module.exports = router;

5. Middleware de Autenticação

Crie src/middleware/authMiddleware.js:

const jwt = require('jsonwebtoken');

module.exports = (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    return res.status(401).json({ error: 'Token não fornecido' });
  }

  const parts = authHeader.split(' ');
  if (parts.length !== 2 || parts[0] !== 'Bearer') {
    return res.status(401).json({ error: 'Formato de token inválido' });
  }

  const token = parts[1];

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // { userId, email, iat, exp }
    return next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expirado' });
    }
    return res.status(401).json({ error: 'Token inválido' });
  }
};

6. Rotas Protegidas e Controle de Acesso

Exemplo de rota protegida para perfil do usuário:

// src/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const authMiddleware = require('../middleware/authMiddleware');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

router.get('/profile', authMiddleware, async (req, res) => {
  try {
    const user = await prisma.user.findUnique({
      where: { id: req.user.userId },
      select: { id: true, email: true, name: true, createdAt: true }
    });

    if (!user) {
      return res.status(404).json({ error: 'Usuário não encontrado' });
    }

    res.json(user);
  } catch (error) {
    res.status(500).json({ error: 'Erro ao buscar perfil' });
  }
});

module.exports = router;

No server.js, aplique o middleware em grupos de rotas:

const express = require('express');
const app = express();

app.use(express.json());
app.use('/api/auth', require('./routes/authRoutes'));
app.use('/api/users', require('./routes/userRoutes')); // todas protegidas pelo middleware na rota

app.listen(3000, () => console.log('Servidor rodando na porta 3000'));

7. Refresh Token e Segurança Adicional

Implemente refresh tokens armazenados em httpOnly cookies:

// No authController.js
exports.refreshToken = async (req, res) => {
  const refreshToken = req.cookies?.refreshToken;

  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token não encontrado' });
  }

  try {
    // Verificar refresh token (deve ter secret diferente)
    const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);

    // Gerar novo access token
    const newAccessToken = jwt.sign(
      { userId: decoded.userId, email: decoded.email },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );

    res.json({ token: newAccessToken });
  } catch (error) {
    return res.status(401).json({ error: 'Refresh token inválido ou expirado' });
  }
};

Boas práticas de segurança:
- Use access tokens com expiração curta (15-30 minutos)
- Armazene refresh tokens em httpOnly cookies (não acessíveis via JavaScript)
- Implemente blacklist de tokens revogados (ex: em Redis ou banco de dados)
- Sempre use HTTPS em produção

8. Integração com React (Frontend)

Crie um contexto de autenticação no React:

// AuthContext.jsx
import { createContext, useState, useEffect } from 'react';
import axios from 'axios';

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(localStorage.getItem('token'));

  useEffect(() => {
    if (token) {
      axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
    }
  }, [token]);

  const login = async (email, password) => {
    const response = await axios.post('/api/auth/login', { email, password });
    const { token, user } = response.data;
    localStorage.setItem('token', token);
    setToken(token);
    setUser(user);
  };

  const logout = () => {
    localStorage.removeItem('token');
    setToken(null);
    setUser(null);
    delete axios.defaults.headers.common['Authorization'];
  };

  return (
    <AuthContext.Provider value={{ user, token, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

Configure interceptors do Axios para renovar token automaticamente:

// axiosInstance.js
import axios from 'axios';

const api = axios.create({ baseURL: 'http://localhost:3000/api' });

api.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        const { data } = await axios.post('/api/auth/refresh-token', {}, { withCredentials: true });
        localStorage.setItem('token', data.token);
        originalRequest.headers['Authorization'] = `Bearer ${data.token}`;
        return api(originalRequest);
      } catch (refreshError) {
        // Redirecionar para login
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

Proteção de rotas no frontend:

// ProtectedRoute.jsx
import { useContext } from 'react';
import { Navigate } from 'react-router-dom';
import { AuthContext } from './AuthContext';

const ProtectedRoute = ({ children }) => {
  const { token } = useContext(AuthContext);
  return token ? children : <Navigate to="/login" />;
};

Conclusão

Implementar autenticação com JWT no Node.js oferece uma solução escalável e stateless para aplicações modernas. Combinando tokens de acesso de curta duração com refresh tokens seguros, você obtém um sistema robusto. A integração com React através de contextos e interceptors do Axios completa o ecossistema, proporcionando uma experiência de usuário fluida e segura.

Referências