Middleware no Next.js: autenticação e redirecionamento

1. Introdução ao Middleware no Next.js

O Middleware no Next.js é uma funcionalidade poderosa que permite executar código antes que uma requisição seja completada. Ele age como um interceptador no ciclo de vida da aplicação, sendo executado antes das rotas e APIs serem processadas. Isso significa que você pode tomar decisões sobre a requisição antes mesmo dela chegar ao seu destino final.

O papel do middleware na arquitetura do Next.js é fundamental para implementar lógicas de segurança, redirecionamento e processamento global. Entre os casos de uso mais comuns estão:

  • Autenticação: verificar tokens JWT antes de permitir acesso a rotas protegidas
  • Redirecionamento: direcionar usuários não autenticados para páginas de login
  • Logging: registrar informações sobre requisições
  • Geolocalização: adaptar conteúdo baseado na localização do usuário
  • A/B Testing: redirecionar usuários para diferentes versões de páginas

2. Configuração e Estrutura Básica do Middleware

Para criar um middleware no Next.js, você precisa criar um arquivo middleware.ts (ou middleware.js) na raiz do seu projeto. A estrutura básica é simples:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Lógica do middleware aqui
  return NextResponse.next()
}

// Configuração de matcher para definir quais rotas interceptar
export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*']
}

O config.matcher é crucial para performance, pois define exatamente quais rotas serão interceptadas. Você pode usar padrões como:
- '/:path*' - todas as rotas
- '/dashboard/:path*' - todas as rotas que começam com /dashboard
- ['/api/:path*', '/admin/:path*'] - múltiplos padrões

3. Implementando Autenticação com Middleware

A autenticação é um dos usos mais comuns do middleware. Vamos implementar um sistema de verificação de tokens JWT armazenados em cookies:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'

const secret = new TextEncoder().encode(process.env.JWT_SECRET)

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value

  // Rotas públicas que não precisam de autenticação
  const publicPaths = ['/login', '/register', '/api/auth']
  if (publicPaths.some(path => request.nextUrl.pathname.startsWith(path))) {
    return NextResponse.next()
  }

  // Verificar token
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  try {
    // Verificar e decodificar o token JWT
    const { payload } = await jwtVerify(token, secret)

    // Adicionar informações do usuário ao header da requisição
    const response = NextResponse.next()
    response.headers.set('x-user-id', payload.sub as string)
    response.headers.set('x-user-role', payload.role as string)

    return response
  } catch (error) {
    // Token inválido ou expirado
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
}

4. Redirecionamento Condicional e Proteção de Rotas

O redirecionamento condicional é essencial para proteger rotas sensíveis. Vamos explorar diferentes cenários:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value
  const url = request.nextUrl.clone()

  // Redirecionar usuário logado para dashboard se tentar acessar login
  if (url.pathname === '/login' && token) {
    url.pathname = '/dashboard'
    return NextResponse.redirect(url)
  }

  // Redirecionar usuário não logado para login com redirect param
  if (!token && url.pathname.startsWith('/dashboard')) {
    url.pathname = '/login'
    url.searchParams.set('redirect', request.nextUrl.pathname)
    return NextResponse.redirect(url)
  }

  // Proteger rotas de admin
  if (url.pathname.startsWith('/admin')) {
    const userRole = request.cookies.get('user-role')?.value
    if (userRole !== 'admin') {
      url.pathname = '/403'
      return NextResponse.rewrite(url)
    }
  }

  return NextResponse.next()
}

5. Tratamento de Rotas Públicas vs. Privadas

Uma boa prática é organizar as rotas em grupos e usar matchers avançados:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const publicRoutes = ['/login', '/register', '/forgot-password', '/api/auth']
const authRoutes = ['/dashboard', '/profile', '/settings']
const adminRoutes = ['/admin']

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value
  const pathname = request.nextUrl.pathname

  // Verificar se é rota pública
  if (publicRoutes.some(route => pathname.startsWith(route))) {
    return NextResponse.next()
  }

  // Verificar rotas autenticadas
  if (authRoutes.some(route => pathname.startsWith(route))) {
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }

  // Verificar rotas de admin
  if (adminRoutes.some(route => pathname.startsWith(route))) {
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
    // Verificar role de admin
  }

  return NextResponse.next()
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|public).*)',
  ]
}

6. Integração com API Routes e Server Actions

O middleware pode se comunicar com API Routes para validação de tokens:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  // Para rotas de API, validar token no header
  if (request.nextUrl.pathname.startsWith('/api/protected')) {
    const authHeader = request.headers.get('authorization')

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return NextResponse.json(
        { error: 'Token não fornecido' },
        { status: 401 }
      )
    }

    const token = authHeader.split(' ')[1]

    try {
      // Validar token com API interna
      const validationResponse = await fetch(
        `${request.nextUrl.origin}/api/auth/validate`,
        {
          headers: { 'Authorization': `Bearer ${token}` }
        }
      )

      if (!validationResponse.ok) {
        throw new Error('Token inválido')
      }

      const userData = await validationResponse.json()
      const response = NextResponse.next()
      response.headers.set('x-user-data', JSON.stringify(userData))

      return response
    } catch (error) {
      return NextResponse.json(
        { error: 'Token inválido ou expirado' },
        { status: 401 }
      )
    }
  }

  return NextResponse.next()
}

7. Performance, Cache e Boas Práticas

O middleware é executado no Edge Runtime por padrão, o que oferece baixa latência mas tem limitações:

// middleware.ts - Boas práticas de performance
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// Cache de configurações
const PUBLIC_ROUTES = new Set(['/login', '/register', '/api/auth'])
const PROTECTED_ROUTES = /^\/(dashboard|profile|settings)/

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname

  // Verificação rápida usando Set
  if (PUBLIC_ROUTES.has(pathname)) {
    return NextResponse.next()
  }

  // Regex para rotas protegidas
  if (PROTECTED_ROUTES.test(pathname)) {
    const token = request.cookies.get('auth-token')?.value
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }

  return NextResponse.next()
}

// Dicas de performance:
// 1. Evite operações assíncronas desnecessárias
// 2. Use estruturas de dados rápidas (Set, Map)
// 3. Mantenha o middleware leve e rápido
// 4. Use matchers específicos ao invés de genéricos

8. Exemplo Completo: Sistema de Autenticação com Redirecionamento

Vamos criar um sistema completo de autenticação:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// Configurações
const AUTH_COOKIE = 'session-token'
const LOGIN_PAGE = '/login'
const DASHBOARD_PAGE = '/dashboard'
const PUBLIC_ROUTES = ['/login', '/register', '/api/auth', '/public']
const PROTECTED_ROUTES = ['/dashboard', '/profile', '/settings']

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  const token = request.cookies.get(AUTH_COOKIE)?.value

  // 1. Rotas públicas
  if (PUBLIC_ROUTES.some(route => pathname.startsWith(route))) {
    // Se usuário já está logado e tenta acessar login, redirecionar
    if (token && pathname === LOGIN_PAGE) {
      return NextResponse.redirect(new URL(DASHBOARD_PAGE, request.url))
    }
    return NextResponse.next()
  }

  // 2. Rotas protegidas
  if (PROTECTED_ROUTES.some(route => pathname.startsWith(route))) {
    if (!token) {
      // Salvar URL de destino para redirect pós-login
      const loginUrl = new URL(LOGIN_PAGE, request.url)
      loginUrl.searchParams.set('callbackUrl', pathname)
      return NextResponse.redirect(loginUrl)
    }

    // Token existe, validar e prosseguir
    try {
      const response = NextResponse.next()
      response.headers.set('x-auth-token', token)
      return response
    } catch (error) {
      // Token inválido, redirecionar para login
      const loginUrl = new URL(LOGIN_PAGE, request.url)
      loginUrl.searchParams.set('error', 'session_expired')
      return NextResponse.redirect(loginUrl)
    }
  }

  // 3. Outras rotas (páginas estáticas, etc.)
  return NextResponse.next()
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|public).*)',
  ]
}
// app/login/page.jsx
'use client'

import { useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'

export default function LoginPage() {
  const router = useRouter()
  const searchParams = useSearchParams()
  const callbackUrl = searchParams.get('callbackUrl') || '/dashboard'
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const handleLogin = async (e) => {
    e.preventDefault()

    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    })

    if (response.ok) {
      // Redirecionar para URL original ou dashboard
      router.push(callbackUrl)
    }
  }

  return (
    <form onSubmit={handleLogin}>
      <input 
        type="email" 
        value={email} 
        onChange={(e) => setEmail(e.target.value)} 
        placeholder="Email" 
      />
      <input 
        type="password" 
        value={password} 
        onChange={(e) => setPassword(e.target.value)} 
        placeholder="Senha" 
      />
      <button type="submit">Entrar</button>
    </form>
  )
}

Este sistema completo demonstra:
- Proteção de rotas sensíveis
- Redirecionamento inteligente pós-login
- Tratamento de sessões expiradas
- Fluxo completo de autenticação

O middleware é uma ferramenta essencial no Next.js moderno, permitindo controle granular sobre o fluxo de autenticação e redirecionamento sem sacrificar performance.

Referências