Ir para o conteúdo
Nextjs

Autenticação Completa no Next.js com NextAuth.js e Middleware

Marcos Soares
Atualizado em 
12 minutos de leitura
Ilustracao 3D de portal de vidro translucido com fechadura representando autenticacao e middleware no Next.js
Ouça este artigo
0:00Autenticação Completa no Next.js com NextAuth.js e Middleware--:--

Conteúdo técnico toda semana

Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.

Sem spam. Cancele a qualquer momento com 1 clique.

Neste artigo

O erro que abre a porta: rotas desprotegidas no App Router

A configuração padrão do NextAuth.js protege exatamente zero rotas. Você instala, configura o provider, faz login funcionar na tela e assume que o resto está seguro. Não está. Qualquer rota server-side ou API route continua acessível sem sessão até que você adicione verificação explícita em cada handler ou configure o middleware corretamente.

Esse post monta uma autenticação completa: configuração do NextAuth.js v5 (Auth.js) no App Router, provider OAuth (GitHub) e credentials, sessão JWT, middleware que protege rotas na edge e callbacks que controlam o que vai no token. O resultado é uma base que você copia, adapta o provider e tem proteção real.

Estrutura do projeto e dependências

Antes de qualquer código, instale o necessário:

Bash
npm install next-auth@beta @auth/prisma-adapter @prisma/client prisma bcryptjs
npm install -D @types/bcryptjs

A versão next-auth@beta é o Auth.js v5, que suporta App Router nativamente. Se você ainda usa Pages Router, a API é diferente. Este post assume App Router com Next.js 14+.

A estrutura de arquivos que vamos construir:

Text
src/
├── auth.ts                    # Configuração central do NextAuth
├── middleware.ts               # Proteção de rotas na edge
├── app/
│   ├── api/auth/[...nextauth]/route.ts
│   ├── (protected)/
│   │   └── dashboard/page.tsx
│   ├── login/page.tsx
│   └── layout.tsx
├── lib/
│   └── db.ts                  # Instância do Prisma
└── components/
    └── session-provider.tsx

Configuração central do NextAuth.js v5

O arquivo auth.ts na raiz do src é o ponto central. Tudo parte daqui:

TypeScript
// src/auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import { PrismaAdapter } from "@auth/prisma-adapter";
import bcrypt from "bcryptjs";
import { db } from "@/lib/db";
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(db),
  // JWT porque o middleware roda na edge e não tem acesso ao banco
  session: { strategy: "jwt" },
  pages: {
    signIn: "/login",
    error: "/login",
  },
  providers: [
    GitHub({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
    Credentials({
      name: "credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Senha", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) return null;
 
        const user = await db.user.findUnique({
          where: { email: credentials.email as string },
        });
 
        if (!user || !user.hashedPassword) return null;
 
        const passwordMatch = await bcrypt.compare(
          credentials.password as string,
          user.hashedPassword
        );
 
        if (!passwordMatch) return null;
 
        // Retorne apenas o que o NextAuth precisa para montar o token
        return {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
        };
      },
    }),
  ],
  callbacks: {
    // Injeta dados customizados no JWT
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role;
        token.id = user.id;
      }
      return token;
    },
    // Expõe dados do JWT na sessão do client
    async session({ session, token }) {
      if (session.user) {
        session.user.role = token.role as string;
        session.user.id = token.id as string;
      }
      return session;
    },
    // Controla redirecionamento pós-login
    async authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith("/dashboard");
 
      if (isOnDashboard && !isLoggedIn) {
        return Response.redirect(new URL("/login", nextUrl));
      }
 
      return true;
    },
  },
});

Três decisões não óbvias aqui. Primeira: session: { strategy: "jwt" } é obrigatório quando você usa middleware, porque o middleware roda no edge runtime e não consegue fazer query ao banco para buscar sessão. Segunda: o callback authorized é o ponto de integração com o middleware (veremos a seguir). Terceira: o PrismaAdapter persiste usuários OAuth no banco, mas a sessão em si vive no JWT.

Route handler e instância do Prisma

TypeScript
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
 
export const { GET, POST } = handlers;
TypeScript
// src/lib/db.ts
import { PrismaClient } from "@prisma/client";
 
// Evita múltiplas instâncias em dev por causa do hot reload
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};
 
export const db = globalForPrisma.prisma ?? new PrismaClient();
 
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;

Middleware: proteção de rotas na edge

O middleware é o ponto onde a autenticação deixa de ser opcional e vira obrigatória. Sem ele, cada page e cada API route precisa verificar sessão individualmente. Com ele, você define um matcher e toda requisição passa pelo filtro antes de chegar ao handler.

TypeScript
// src/middleware.ts
import { auth } from "@/auth";
 
export default auth;
 
// Protege tudo exceto assets estáticos, imagens e rotas públicas
export const config = {
  matcher: [
    "/((?!api/auth|_next/static|_next/image|favicon.ico|login|register|$).*)",
  ],
};

Esse regex no matcher exclui as rotas do próprio NextAuth (api/auth), assets do Next.js, a página de login, registro e a home ($). Tudo que não bate nessas exceções passa pelo middleware, que executa o callback authorized definido no auth.ts.

Se você precisa de lógica mais granular (proteger /admin só para role admin, por exemplo), o middleware sozinho não resolve com elegância. A abordagem que funciona é combinar middleware para autenticação básica (está logado?) com verificação de role dentro do Server Component ou API route. Tentamos detalhar essa estratégia em Next.js Middleware: Autenticação, A/B Testing e Geolocalização na Edge.

Tipagem customizada da sessão

O TypeScript reclama quando você acessa session.user.role porque o tipo padrão não inclui campos customizados. Resolva com module augmentation:

TypeScript
// src/types/next-auth.d.ts
import { DefaultSession } from "next-auth";
 
declare module "next-auth" {
  interface Session {
    user: {
      id: string;
      role: string;
    } & DefaultSession["user"];
  }
 
  interface User {
    role: string;
  }
}
 
declare module "@auth/core/jwt" {
  interface JWT {
    role: string;
    id: string;
  }
}

Sem essa declaração, o TypeScript compila mas você perde type safety nos callbacks e nos componentes que consomem a sessão.

Consumindo a sessão no Server Component e no Client Component

No App Router, existem dois caminhos para acessar a sessão. Escolha com base em onde o componente roda:

ContextoFunçãoRuntimeAcesso ao banco
Server Componentauth()Node.jsSim
Client ComponentuseSession()BrowserNão (usa JWT decodificado)
Middlewarecallback authorizedEdgeNão
API Route (Route Handler)auth()Node.jsSim

Server Component:

TSX
// src/app/(protected)/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
 
export default async function DashboardPage() {
  const session = await auth();
 
  // Dupla verificação: mesmo com middleware, valide no componente
  if (!session?.user) {
    redirect("/login");
  }
 
  return (
    <main>
      <h1>Dashboard</h1>
      <p>Logado como {session.user.email}</p>
      <p>Role: {session.user.role}</p>
    </main>
  );
}

Client Component com SessionProvider:

TSX
// src/components/session-provider.tsx
"use client";
 
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
 
export function SessionProvider({ children }: { children: React.ReactNode }) {
  return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
}
TSX
// src/app/layout.tsx
import { SessionProvider } from "@/components/session-provider";
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="pt-BR">
      <body>
        <SessionProvider>{children}</SessionProvider>
      </body>
    </html>
  );
}

O SessionProvider envolve o layout raiz. Componentes client acessam a sessão via useSession() sem prop drilling.

Página de login com signIn server action

TSX
// src/app/login/page.tsx
"use client";
 
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
 
export default function LoginPage() {
  const router = useRouter();
  const [error, setError] = useState<string | null>(null);
 
  async function handleCredentialsLogin(formData: FormData) {
    setError(null);
 
    const result = await signIn("credentials", {
      email: formData.get("email") as string,
      password: formData.get("password") as string,
      redirect: false,
    });
 
    if (result?.error) {
      setError("Email ou senha inválidos");
      return;
    }
 
    router.push("/dashboard");
    router.refresh();
  }
 
  return (
    <div>
      <h1>Login</h1>
 
      <button onClick={() => signIn("github", { callbackUrl: "/dashboard" })}>
        Entrar com GitHub
      </button>
 
      <form action={handleCredentialsLogin}>
        <input name="email" type="email" placeholder="Email" required />
        <input name="password" type="password" placeholder="Senha" required />
        {error && <p style={{ color: "red" }}>{error}</p>}
        <button type="submit">Entrar</button>
      </form>
    </div>
  );
}

O redirect: false no signIn de credentials é proposital: permite capturar o erro e exibir feedback inline em vez de redirecionar para a página de erro padrão do NextAuth.

O que NÃO fazer

Anti-pattern 1: verificar sessão só no client

Código errado:

TSX
// ERRADO: proteção apenas no client
"use client";
import { useSession } from "next-auth/react";
import { redirect } from "next/navigation";
 
export default function AdminPage() {
  const { data: session, status } = useSession();
 
  if (status === "loading") return <p>Carregando...</p>;
  if (!session) redirect("/login");
 
  return <h1>Admin</h1>;
}

O problema: o HTML da página é renderizado no servidor e enviado ao browser antes do useSession executar. Um usuário não autenticado recebe o HTML completo da página admin por uma fração de segundo (flash of content). Pior: se a página tiver dados sensíveis no HTML inicial, eles vazam no source da resposta HTTP.

Código correto: use auth() no Server Component (como mostrado acima) ou middleware. A verificação acontece antes do HTML ser gerado.

Anti-pattern 2: armazenar senha no JWT

TypeScript
// ERRADO: nunca coloque dados sensíveis no token
async jwt({ token, user }) {
  if (user) {
    token.password = user.hashedPassword; // NUNCA
    token.creditCard = user.creditCard;   // NUNCA
  }
  return token;
}

O JWT é assinado, não criptografado (por padrão). Qualquer pessoa com acesso ao cookie consegue decodificar o payload em base64 e ler o conteúdo. Coloque no token apenas identificadores e roles. Dados sensíveis ficam no banco, acessados via auth() no server-side quando necessário.

Anti-pattern 3: matcher do middleware que captura tudo

TypeScript
// ERRADO: bloqueia assets, API de auth e causa loop de redirect
export const config = {
  matcher: ["/:path*"],
};

Isso intercepta até as rotas do próprio NextAuth (/api/auth/session, /api/auth/callback), causando loop infinito de redirecionamento. Sempre exclua api/auth, _next/static e _next/image do matcher.

Credentials vs. OAuth: quando usar cada um

CritérioCredentialsOAuth (GitHub, Google)
Complexidade de implementaçãoAlta (hash, validação, reset de senha)Baixa (provider pronto)
Segurança por padrãoDepende da sua implementação de hash/rate limitDelegada ao provider
Experiência do usuárioFamiliar, mas exige cadastroUm clique, sem senha
Compatibilidade com PrismaAdapterParcial (adapter não persiste sessão de credentials)Total
Quando usarApps corporativos que exigem login interno, ou quando OAuth não é opçãoSaaS, ferramentas dev, qualquer app onde o usuário já tem conta Google/GitHub

Se você tem menos de 1000 usuários e controle total do ambiente, credentials funciona. Acima disso, a superfície de ataque de gerenciar senhas (brute force, credential stuffing, reset de senha, vazamento de hash) pesa contra. OAuth delega esse problema para quem já investiu bilhões em segurança.

Para proteger APIs que recebem tráfego externo, a camada de autenticação precisa conversar com rate limiting e caching. Detalhamos esses patterns em API Gateway Patterns: Autenticação, Rate Limiting e Caching com Node.js.

Variáveis de ambiente

Bash
# .env.local
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=gere-com-openssl-rand-base64-32
GITHUB_CLIENT_ID=seu_client_id
GITHUB_CLIENT_SECRET=seu_client_secret
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb

O NEXTAUTH_SECRET assina os JWTs. Gere com openssl rand -base64 32. Em produção, essa variável precisa estar configurada no ambiente do deploy, não commitada no repositório. Se você usa Docker, configure via environment no compose, como mostramos em Docker para Devs: do Dockerfile ao docker-compose em Produção.

Protegendo API Routes

TypeScript
// src/app/api/users/route.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
 
export async function GET() {
  const session = await auth();
 
  if (!session?.user) {
    return NextResponse.json({ error: "Não autorizado" }, { status: 401 });
  }
 
  if (session.user.role !== "admin") {
    return NextResponse.json({ error: "Sem permissão" }, { status: 403 });
  }
 
  const users = await db.user.findMany({
    select: { id: true, email: true, name: true, role: true },
  });
 
  return NextResponse.json(users);
}

Separe autenticação (401: quem é você?) de autorização (403: você pode fazer isso?). O middleware cuida do primeiro. O handler cuida do segundo. Misturar os dois no mesmo lugar gera confusão quando o sistema cresce.

Para aplicações que processam dados pesados após a autenticação, considere delegar o trabalho para filas. Cobrimos esse padrão em Mensageria com BullMQ e Redis: Processamento Assíncrono que Funciona.

Se a autenticação faz parte de um projeto maior com múltiplos pacotes, a configuração do NextAuth pode ser compartilhada via monorepo. A estrutura que descrevemos em Monorepos com Turborepo: Estrutura, Cache e Deploy se aplica diretamente.

FAQ

O NextAuth.js v5 (Auth.js) é estável para produção? A API está em beta, mas é usada em produção por projetos relevantes. O risco real é breaking change entre versões beta. Trave a versão no package.json com versão exata (sem ^) e leia o changelog antes de atualizar.

Posso usar database session em vez de JWT? Pode, mas perde a capacidade de verificar sessão no middleware (edge runtime não acessa banco). Se você não usa middleware para proteção de rotas, database session funciona e tem a vantagem de invalidação imediata (deletou a sessão do banco, o usuário perde acesso na próxima request).

Como implemento refresh token com NextAuth? O NextAuth gerencia refresh tokens automaticamente para providers OAuth que os fornecem. Para credentials, não existe refresh token nativo. A abordagem é configurar maxAge no JWT e forçar re-autenticação quando expira. Se precisa de refresh silencioso com credentials, considere implementar sua própria lógica no callback jwt verificando token.exp.

O Supabase Auth substitui o NextAuth? São abordagens diferentes. Supabase Auth é um serviço gerenciado com Row Level Security integrada ao banco. NextAuth é uma biblioteca que você hospeda junto da aplicação. Se já usa Supabase como backend, faz sentido usar o auth deles. Detalhamos essa integração em Supabase como Backend Completo para Next.js. Se usa Prisma com PostgreSQL próprio, NextAuth dá mais controle.

Preciso do middleware E da verificação no Server Component? Sim. O middleware é a primeira barreira, rápida e na edge. A verificação no Server Component é a segunda, com acesso ao banco para checar roles, permissões granulares ou estado do usuário. Defesa em profundidade: se uma camada falha, a outra segura.

Posição técnica

NextAuth.js resolve autenticação para 90% dos projetos Next.js. Os 10% restantes são aplicações que precisam de controle fino sobre tokens, integração com identity providers corporativos (SAML, LDAP) ou fluxos de autenticação não convencionais. Para esses casos, bibliotecas como oslo ou implementação manual com jose para JWTs dão mais controle, ao custo de mais código para manter.

A decisão se resume a isso: se o seu fluxo de auth cabe em "login com provider, sessão JWT, proteção de rotas", use NextAuth e invista seu tempo no produto. Se você precisa customizar o fluxo de tokens, emitir tokens para terceiros ou integrar com SSO corporativo, NextAuth vai te atrapalhar mais do que ajudar a partir de certo ponto.

Marcos Soares

Escrito por

Marcos Soares

Fullstack Developer · CEO da Agência Poti

Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.

Comentários

Participe da discussão

Seja o primeiro a comentar!

Continue Aprofundando

Guias de integração relacionados

Conteúdo técnico toda semana

Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.

Sem spam. Cancele a qualquer momento com 1 clique.