Middlewares de autenticação e autorização

1. Conceitos Fundamentais de Autenticação e Autorização

Antes de implementar middlewares, é crucial entender a diferença entre autenticação e autorização. Autenticação responde "quem é você?" — verifica a identidade do usuário através de credenciais como email/senha ou tokens. Autorização responde "o que você pode fazer?" — determina quais recursos ou ações o usuário autenticado tem permissão para acessar.

O JWT (JSON Web Token) é o padrão mais utilizado para autenticação stateless. Sua estrutura é composta por três partes separadas por pontos:

  • Header: contém o tipo do token e o algoritmo de hash (ex: HS256)
  • Payload: contém as claims (dados do usuário, expiração, etc.)
  • Signature: hash criptográfico que garante integridade

O fluxo típico é: login → servidor valida credenciais → gera JWT → cliente armazena token → envia em requisições protegidas via header Authorization.

2. Implementando um Middleware de Autenticação com JWT

Vamos criar um middleware que extrai e valida o token JWT do header Authorization (formato Bearer):

const jwt = require('jsonwebtoken');

const authenticate = (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; // Injeta dados do usuário na requisição
    return next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expirado' });
    }
    return res.status(401).json({ error: 'Token inválido' });
  }
};

module.exports = authenticate;

3. Middleware de Autorização por Papéis (Roles)

Agora implementamos um middleware que verifica se o usuário possui a role necessária. Usaremos uma factory function que aceita as roles permitidas:

const authorize = (...allowedRoles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Usuário não autenticado' });
    }

    const { role } = req.user;

    if (!allowedRoles.includes(role)) {
      return res.status(403).json({ 
        error: 'Acesso negado. Permissão insuficiente' 
      });
    }

    return next();
  };
};

module.exports = authorize;

Exemplo de uso em rotas:

const express = require('express');
const router = express.Router();
const authenticate = require('./middlewares/authenticate');
const authorize = require('./middlewares/authorize');

// Rota protegida para qualquer usuário autenticado
router.get('/profile', authenticate, (req, res) => {
  res.json({ user: req.user });
});

// Rota exclusiva para administradores
router.delete('/users/:id', authenticate, authorize('admin'), async (req, res) => {
  // Lógica de exclusão
});

// Rota para admin ou moderator
router.put('/posts/:id', authenticate, authorize('admin', 'moderator'), async (req, res) => {
  // Lógica de atualização
});

4. Gerenciamento de Sessões e Refresh Tokens

Tokens JWT precisam de curta duração (15-30 minutos) para segurança. Implementamos refresh tokens para renovação automática:

const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// Armazenamento em memória (substituir por Redis em produção)
const refreshTokens = new Map();

const generateTokens = (user) => {
  const accessToken = jwt.sign(
    { id: user.id, email: user.email, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRES_IN || '15m' }
  );

  const refreshToken = crypto.randomBytes(40).toString('hex');
  refreshTokens.set(refreshToken, {
    userId: user.id,
    expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000 // 7 dias
  });

  return { accessToken, refreshToken };
};

const refreshTokenMiddleware = async (req, res) => {
  const { refreshToken } = req.body;

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

  const storedToken = refreshTokens.get(refreshToken);

  if (!storedToken || storedToken.expiresAt < Date.now()) {
    refreshTokens.delete(refreshToken);
    return res.status(401).json({ error: 'Refresh token inválido ou expirado' });
  }

  // Rotação de token: remove o antigo e gera novo
  refreshTokens.delete(refreshToken);

  const user = await User.findById(storedToken.userId);
  if (!user) {
    return res.status(401).json({ error: 'Usuário não encontrado' });
  }

  const tokens = generateTokens(user);

  res.cookie('refreshToken', tokens.refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000
  });

  return res.json({ accessToken: tokens.accessToken });
};

5. Proteção de Rotas no Frontend com React

No lado do React, criamos um sistema de autenticação com Context API e componentes protegidos:

// AuthContext.js
import React, { createContext, useContext, useState, useEffect } from 'react';
import api from './api';

const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const token = localStorage.getItem('accessToken');
    if (token) {
      api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
      // Verificar token atual
      api.get('/auth/me')
        .then(response => setUser(response.data))
        .catch(() => logout())
        .finally(() => setLoading(false));
    } else {
      setLoading(false);
    }
  }, []);

  const login = async (email, password) => {
    const response = await api.post('/auth/login', { email, password });
    const { accessToken, refreshToken } = response.data;
    localStorage.setItem('accessToken', accessToken);
    api.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
    setUser(response.data.user);
  };

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

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

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth deve ser usado dentro de AuthProvider');
  }
  return context;
};
// PrivateRoute.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { useAuth } from './AuthContext';

const PrivateRoute = ({ component: Component, roles, ...rest }) => {
  const { user, loading } = useAuth();

  if (loading) {
    return <div>Carregando...</div>;
  }

  return (
    <Route
      {...rest}
      render={props => {
        if (!user) {
          return <Redirect to="/login" />;
        }

        if (roles && !roles.includes(user.role)) {
          return <Redirect to="/unauthorized" />;
        }

        return <Component {...props} />;
      }}
    />
  );
};

export default PrivateRoute;
// Configuração do Axios com interceptors para refresh automático
import axios from 'axios';

const api = axios.create({
  baseURL: process.env.REACT_APP_API_URL
});

api.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const refreshToken = localStorage.getItem('refreshToken');
        const response = await api.post('/auth/refresh', { refreshToken });
        const { accessToken } = response.data;

        localStorage.setItem('accessToken', accessToken);
        api.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
        originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;

        return api(originalRequest);
      } catch (refreshError) {
        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

export default api;

6. Boas Práticas e Segurança

Para garantir a segurança da sua implementação:

// Configuração segura
require('dotenv').config();

// Validação de entrada no middleware
const validateLoginInput = (req, res, next) => {
  const { email, password } = req.body;

  if (!email || !password) {
    return res.status(400).json({ error: 'Email e senha são obrigatórios' });
  }

  // Sanitização básica
  req.body.email = email.trim().toLowerCase();
  next();
};

// Rate limiting para prevenir brute force
const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 5, // 5 tentativas
  message: { error: 'Muitas tentativas de login. Tente novamente mais tarde' }
});

// Blacklist de tokens (exemplo com Redis)
const redis = require('redis');
const client = redis.createClient();

const logout = async (req, res) => {
  const token = req.headers.authorization.split(' ')[1];
  const decoded = jwt.decode(token);

  // Adiciona token à blacklist até expirar
  await client.set(`blacklist:${token}`, 'true', 'EX', decoded.exp - Math.floor(Date.now() / 1000));

  res.json({ message: 'Logout realizado com sucesso' });
};

Resumo das boas práticas:
- Armazene JWT_SECRET em variáveis de ambiente
- Use httpOnly cookies para refresh tokens
- Implemente rate limiting em rotas de login
- Faça validação e sanitização de todos os inputs
- Use blacklist de tokens para logout efetivo
- Mantenha access tokens com curta duração (15-30 min)
- Rode refresh tokens a cada uso

Referências