Este guia aborda a implementação completa de autenticação com Next.js usando NextAuth.js, incluindo login social com Google, login com credenciais e autenticação de dois fatores (2FA).
A autenticação de dois fatores (2FA) adiciona uma camada extra de segurança ao processo de login. Com NextAuth.js, podemos integrar facilmente tanto login social quanto com credenciais, além de adicionar 2FA ao fluxo de autenticação.
npm install next-auth @auth/core @next-auth/prisma-adapter prisma
npm install react-totp-input qrcode otplib
src/
├── app/
│ ├── api/
│ │ └── auth/
│ │ └── [...nextauth]/
│ │ └── route.ts
│ ├── login/
│ │ └── page.tsx
│ ├── auth/
│ │ ├── verify-2fa/
│ │ │ └── page.tsx
│ │ └── error/
│ │ └── page.tsx
│ └── profile/
│ └── two-factor/
│ └── page.tsx
├── components/
│ ├── LoginForm.tsx
│ ├── TwoFactorForm.tsx
│ └── TwoFactorSetup.tsx
├── lib/
│ └── totp.ts
└── middleware.ts
Arquivo: src/app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
export const authOptions = {
adapter: PrismaAdapter(prisma),
pages: {
signIn: "/login",
error: "/auth/error",
verifyRequest: "/auth/verify-request",
newUser: "/auth/new-user"
},
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 dias
},
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code"
}
}
}),
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Senha", type: "password" },
totpCode: { label: "Código 2FA", type: "text" }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
// Chamada à API para validar credenciais
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: credentials.email,
password: credentials.password,
}),
});
const user = await response.json();
if (!response.ok || !user) {
throw new Error("Credenciais inválidas");
}
// Verificar se o usuário tem 2FA habilitado
if (user.twoFactorEnabled) {
// Se não enviou código TOTP, retornar usuário com flag
if (!credentials.totpCode) {
return {
...user,
requiresTwoFactor: true
};
}
// Verificar código TOTP
const totpResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/verify-totp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
userId: user.id,
code: credentials.totpCode
}),
});
if (!totpResponse.ok) {
throw new Error("Código 2FA inválido");
}
}
return user;
}
}),
],
callbacks: {
// Callbacks definidos na próxima seção
},
events: {
async signIn(message) {
// Registrar logins bem-sucedidos
},
async signOut(message) {
// Limpar tokens/sessões
},
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Os callbacks são funções que são chamadas durante o fluxo de autenticação e permitem personalizar o comportamento do NextAuth.js. Vamos detalhar os principais callbacks:
Este callback é executado sempre que um token JWT é criado (ao fazer login) ou verificado (em cada requisição).
async jwt({ token, user, account }) {
// Quando o usuário faz login, transferir propriedades para o token
if (user) {
token.userId = user.id;
token.role = user.role;
token.requiresTwoFactor = user.requiresTwoFactor;
}
// Se o token indicar que o usuário precisa completar 2FA
if (token.requiresTwoFactor) {
return { ...token, requiresTwoFactor: true };
}
return token;
}
Este callback é executado sempre que uma sessão é verificada (em cada requisição que usa useSession() ou getServerSession()).
async session({ session, token }) {
// Se ainda precisar de verificação 2FA, não gerar sessão válida
if (token.requiresTwoFactor) {
return null;
}
// Adicionar dados do token à sessão
if (token) {
session.user.id = token.userId;
session.user.role = token.role;
}
return session;
}
Este callback controla os redirecionamentos durante o fluxo de autenticação.
async redirect({ url, baseUrl }) {
// Personalizar redirecionamentos após login
if (url.startsWith("/")) return `${baseUrl}${url}`;
else if (new URL(url).origin === baseUrl) return url;
return baseUrl;
}
A configuração de sessão determina como as sessões de usuário são gerenciadas:
session: {
strategy: "jwt", // Usar tokens JWT em vez de sessão no banco de dados
maxAge: 30 * 24 * 60 * 60, // 30 dias de duração
}
Com NextAuth.js, você pode escolher entre duas estratégias de sessão:
Database Sessions (default):
JWT Sessions:
Para 2FA, geralmente usamos JWT para armazenar o estado de autenticação temporário.
/login/loginrequiresTwoFactor: true/auth/verify-2faArquivo: src/lib/totp.ts
import { authenticator } from "otplib";
// Configuração para o TOTP
authenticator.options = {
digits: 6,
step: 30, // 30 segundos
window: 1, // tolerância de 1 passo
};
export function generateTOTP(secret: string) {
return authenticator.generate(secret);
}
export function verifyTOTP(token: string, secret: string) {
return authenticator.verify({ token, secret });
}
export function generateTOTPSecret() {
return authenticator.generateSecret();
}
export function generateTOTPUri(secret: string, email: string, issuer = "SeuApp") {
return authenticator.keyuri(email, issuer, secret);
}
Para suportar 2FA, seu modelo de usuário deve incluir:
model User {
id String @id @default(cuid())
// ... outros campos
twoFactorEnabled Boolean @default(false)
twoFactorSecret String? // Armazenado criptografado
backupCodes String[] // Códigos de recuperação
}
// src/components/LoginForm.tsx
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import TwoFactorForm from "./TwoFactorForm";
export default function LoginForm() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [showTwoFactor, setShowTwoFactor] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError("");
try {
const result = await signIn("credentials", {
redirect: false,
email,
password,
});
if (result?.error) {
// Verificar se é um erro de 2FA
const errorData = JSON.parse(result.error);
if (errorData.requiresTwoFactor) {
setShowTwoFactor(true);
return;
}
setError("Email ou senha inválidos");
return;
}
if (result?.url) {
router.push("/dashboard");
}
} catch (error) {
setError("Ocorreu um erro ao fazer login");
} finally {
setLoading(false);
}
};
const handleGoogleLogin = () => {
signIn("google", { callbackUrl: "/dashboard" });
};
if (showTwoFactor) {
return <TwoFactorForm email={email} password={password} />;
}
// Renderização do formulário...
}
// src/components/TwoFactorForm.tsx
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import ReactTOTPInput from "react-totp-input";
export default function TwoFactorForm({ email, password }) {
const router = useRouter();
const [totpCode, setTotpCode] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError("");
try {
const result = await signIn("credentials", {
redirect: false,
email,
password,
totpCode,
});
if (result?.error) {
setError("Código 2FA inválido");
return;
}
if (result?.url) {
router.push("/dashboard");
}
} catch (error) {
setError("Ocorreu um erro ao verificar o código");
} finally {
setLoading(false);
}
};
// Renderização do formulário...
}
O middleware verifica se o usuário está autenticado e se já completou o 2FA.
// src/middleware.ts
import { NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
export async function middleware(req) {
const token = await getToken({ req });
// Verificar se o usuário está autenticado
if (!token) {
const url = new URL('/login', req.url);
url.searchParams.set('callbackUrl', req.url);
return NextResponse.redirect(url);
}
// Verificar se o usuário precisa completar 2FA
if (token.requiresTwoFactor) {
return NextResponse.redirect(new URL('/auth/verify-2fa', req.url));
}
return NextResponse.next();
}
// Configurar quais rotas o middleware protege
export const config = {
matcher: [
'/dashboard/:path*',
'/profile/:path*',
'/api/protected/:path*',
],
};
Sempre ofereça códigos de recuperação como backup:
function generateRecoveryCodes() {
const codes = [];
for (let i = 0; i < 10; i++) {
// Gerar código aleatório (exemplo: XXXX-XXXX-XXXX)
const code = [...Array(12)]
.map(() => Math.floor(Math.random() * 16).toString(16))
.join('')
.toUpperCase()
.match(/.{1,4}/g)
.join('-');
codes.push(code);
}
return codes;
}
Implemente limitação de tentativas para evitar ataques de força bruta:
// Exemplo simples de rate limiting
const attempts = {};
function checkRateLimit(userId) {
const now = Date.now();
if (!attempts[userId]) {
attempts[userId] = { count: 1, timestamp: now };
return true;
}
// Resetar tentativas após 15 minutos
if (now - attempts[userId].timestamp > 15 * 60 * 1000) {
attempts[userId] = { count: 1, timestamp: now };
return true;
}
// Limitar a 5 tentativas
if (attempts[userId].count >= 5) {
return false;
}
attempts[userId].count += 1;
return true;
}
A implementação de NextAuth com 2FA adiciona uma camada significativa de segurança à sua aplicação. Com esta estrutura, você tem um sistema robusto que suporta múltiplos provedores de autenticação e protege as contas dos usuários com autenticação de dois fatores.
Lembre-se de adaptar este guia à sua API e às necessidades específicas do seu projeto.