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
matcherpara filtrar rotas. NextAuth.js/Auth.js suporta múltiplos providers e JWT/database sessions. Combine middleware com auth() para proteger rotas.