kb.erickguedes.com
Next.js: React Full-Stack

Autenticação e Middleware

Aula 4 de 6

NextAuth.js / Auth.js

npm install next-auth@beta @auth/prisma-adapter

Configuração

// src/lib/auth.ts
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import Google from 'next-auth/providers/google';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { db } from '@/lib/db';
import bcrypt from 'bcryptjs';

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(db),
  session: { strategy: 'jwt' },
  pages: {
    signIn: '/login',
    error: '/error'
  },
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET
    }),
    Credentials({
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Senha', type: 'password' }
      },
      async authorize(credentials) {
        const { email, password } = credentials as {
          email: string;
          password: string;
        };

        const user = await db.user.findUnique({ where: { email } });
        if (!user || !user.password) return null;

        const valid = await bcrypt.compare(password, user.password);
        if (!valid) return null;

        return { id: user.id, name: user.name, email: user.email, role: user.role };
      }
    })
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role;
        token.id = user.id;
      }
      return token;
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.role = token.role as string;
        session.user.id = token.id as string;
      }
      return session;
    }
  }
});
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/lib/auth';

export const { GET, POST } = handlers;

Login e Logout

// app/login/page.tsx
import { signIn } from '@/lib/auth';
import { AuthForm } from './auth-form';

export default async function LoginPage() {
  return (
    <div className="max-w-md mx-auto mt-10 p-6 border rounded">
      <h1>Login</h1>
      <AuthForm />
    </div>
  );
}
// app/login/auth-form.tsx
'use client';

import { signIn } from '@/lib/auth-client';
import { useActionState } from 'react';

export function AuthForm() {
  return (
    <form action={async (formData: FormData) => {
      await signIn('credentials', formData);
    }}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Senha" required />
      <button type="submit">Entrar</button>
    </form>
  );
}

Proteção com auth()

// app/dashboard/page.tsx
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const session = await auth();

  if (!session?.user) {
    redirect('/login');
  }

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Bem-vindo, {session.user.name}!</p>
      <p>Role: {session.user.role}</p>
    </div>
  );
}

Middleware

Middleware intercepta requisições antes de atingirem as rotas.

// middleware.ts
import { auth } from '@/lib/auth';
import { NextResponse } from 'next/server';

export default auth((req) => {
  const { nextUrl } = req;
  const isLoggedIn = !!req.auth;
  const isLoginPage = nextUrl.pathname === '/login';
  const isApiAuth = nextUrl.pathname.startsWith('/api/auth');

  // Não proteger rotas de auth
  if (isApiAuth) return;

  // Redirecionar para login se não autenticado
  if (!isLoggedIn && !isLoginPage) {
    return NextResponse.redirect(new URL('/login', nextUrl));
  }

  // Redirecionar para dashboard se já logado no login
  if (isLoggedIn && isLoginPage) {
    return NextResponse.redirect(new URL('/dashboard', nextUrl));
  }

  return;
});

// Configurar matcher para não executar em arquivos estáticos
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
};

Middleware Avançado (RBAC + Rate Limit)

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

const rateLimit = new Map<string, { count: number; resetTime: number }>();

function checkRateLimit(ip: string, limit = 10, windowMs = 60000): boolean {
  const now = Date.now();
  const record = rateLimit.get(ip);

  if (!record || now > record.resetTime) {
    rateLimit.set(ip, { count: 1, resetTime: now + windowMs });
    return true;
  }

  if (record.count >= limit) return false;

  record.count++;
  return true;
}

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

  // Rate limiting (API)
  if (pathname.startsWith('/api/')) {
    const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
    if (!checkRateLimit(ip)) {
      return NextResponse.json(
        { error: 'Muitas requisições' },
        { status: 429 }
      );
    }
  }

  // i18n routing
  const locales = ['pt-BR', 'en'];
  const defaultLocale = 'pt-BR';
  const pathLocale = locales.find(loc => pathname.startsWith(`/${loc}`));

  if (!pathLocale) {
    return NextResponse.redirect(
      new URL(`/${defaultLocale}${pathname}`, request.url)
    );
  }

  return NextResponse.next();
}

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

Middleware com i18n

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

const locales = ['pt-BR', 'en', 'es'];
const defaultLocale = 'pt-BR';

function getLocale(request: NextRequest): string {
  const acceptLang = request.headers.get('accept-language');
  if (!acceptLang) return defaultLocale;

  const preferred = acceptLang.split(',')[0].split('-')[0];
  const matched = locales.find(l => l.startsWith(preferred));
  return matched || defaultLocale;
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const pathnameHasLocale = locales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (pathnameHasLocale) return;

  const locale = getLocale(request);
  request.nextUrl.pathname = `/${locale}${pathname}`;
  return NextResponse.redirect(request.nextUrl);
}

Lab: Exercício - Autenticação Completa

// app/layout.tsx — SessionProvider
import { SessionProvider } from 'next-auth/react';
import { auth } from '@/lib/auth';

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await auth();

  return (
    <html>
      <body>
        <SessionProvider session={session}>
          {children}
        </SessionProvider>
      </body>
    </html>
  );
}
// components/auth/sign-out.tsx
'use client';

import { signOut } from '@/lib/auth-client';

export function SignOutButton() {
  return (
    <button onClick={() => signOut({ callbackUrl: '/login' })}>
      Sair
    </button>
  );
}
# Testar fluxo de autenticação
# 1. Acessar /dashboard sem login -> redirect para /login
# 2. Fazer login com credenciais
# 3. Acessar /dashboard autenticado
# 4. Fazer logout

Middleware em Next.js é executado na edge para todas as requisições. Use matcher para filtrar rotas. NextAuth.js/Auth.js suporta múltiplos providers e JWT/database sessions. Combine middleware com auth() para proteger rotas.