Autenticação segura com Claude Code: sessões Next.js, JWT e OAuth
Implemente autenticação segura com Claude Code: sessões Next.js, JWT, OAuth, CSRF, RBAC, auditoria e testes.
Autenticação não é apenas uma tela de login. Antes de publicar, você precisa cobrir armazenamento de senhas, sessões, cookies, JWT, callbacks OAuth, CSRF, redefinição de senha, RBAC, secrets, logs de auditoria e testes. Se o pedido para Claude Code for só “adicione auth”, ele pode gerar uma tela que funciona e ainda deixar riscos sérios no desenho.
O atalho perigoso mais comum é guardar JWT de longa duração em localStorage. Se houver XSS, ou seja, JavaScript malicioso rodando dentro da página, o token pode ser lido e reutilizado. Para apps web comuns, uma sessão no servidor com um identificador opaco em cookie HttpOnly costuma ser mais fácil de revogar, auditar e testar.
Este guia usa Next.js App Router e traz um exemplo copiável com validação Zod, hash bcrypt, cookie de sessão assinado, middleware guard, proteção CSRF, verificação de Origin, RBAC, audit log e testes com Vitest. JWT fica restrito a APIs e clientes móveis com vida curta. OAuth é tratado como fronteira de identity provider, não como substituto da autorização local.
Use sempre as fontes oficiais: Next.js Authentication guide, cookies API do Next.js, OWASP Authentication Cheat Sheet, OWASP Password Storage Cheat Sheet, OWASP Forgot Password Cheat Sheet, MDN Secure cookie configuration, Auth.js e Claude Code docs. No ClaudeCodeLab, leia também gerenciamento de cookies, RBAC e validação com Zod.
Separar sessão, JWT e OAuth
Uma sessão responde “este navegador ainda está logado?”. Um JWT responde “este cliente consegue provar claims assinados por pouco tempo?”. OAuth/OIDC responde “um provedor confiável autenticou essa pessoa?”. Misturar essas responsabilidades dificulta review e incident response.
| Mecanismo | Melhor uso | Força | Risco a controlar |
|---|---|---|---|
| Sessão no servidor | Dashboard SaaS, área de membros, admin | Revogação e auditoria simples | Precisa de Redis, Postgres, DynamoDB ou outro store |
| JWT | API móvel, chamadas curtas entre serviços, API externa | Verificação sem consulta ao banco | Revogação difícil; evite armazenamento longo no browser |
| OAuth / OIDC | Google, GitHub, SSO corporativo | Você não guarda a senha do usuário | Login externo não substitui permissões locais |
Três casos práticos ajudam. Em SaaS, a sessão web fica em cookie HttpOnly e mudanças de billing ou e-mail pedem reautenticação. Em conteúdo pago, leitores gratuitos, compradores, editores e admins ficam separados por RBAC e audit log. Em ferramenta B2B interna, Google Workspace ou Entra ID podem autenticar, mas tenant, role, expiração e revogação continuam na aplicação.
Prompt seguro para Claude Code
Antes do código, entregue o contrato de segurança.
Implemente autenticação para Next.js App Router.
Requisitos:
- Login web com sessão no servidor e cookie HttpOnly
- JWT apenas para tokens curtos de API externa; não usar localStorage
- Senhas com bcrypt ou Argon2id, nunca texto puro
- Validação com Zod e sem revelar se o e-mail existe
- Cookies com Secure, HttpOnly, SameSite, Path e Max-Age explícitos
- APIs que mudam estado com Origin check e token CSRF
- OAuth com Auth.js ou biblioteca madura, sem implementação caseira completa
- Incluir RBAC, password reset, audit logs e testes
- Listar pitfalls e comandos de verificação
Esse prompt força Claude Code a trabalhar dentro de fronteiras revisáveis. Autenticação segura começa quando os atalhos perigosos já foram proibidos.
Implementação mínima em Next.js
Instale dependências.
npm install zod bcryptjs
npm install -D vitest typescript @types/node
Defina um SESSION_SECRET com pelo menos 32 caracteres em .env.local e não faça commit.
SESSION_SECRET="replace-with-at-least-32-random-characters"
Arquivo lib/auth/password.ts. OWASP recomenda Argon2id como primeira opção moderna; aqui usamos bcryptjs por ser simples de copiar.
import bcrypt from "bcryptjs";
import { z } from "zod";
export const passwordSchema = z.string().min(12).max(128);
export async function hashPassword(password: string) {
const parsed = passwordSchema.parse(password);
return bcrypt.hash(parsed, 12);
}
export async function verifyPassword(password: string, hash: string) {
return bcrypt.compare(password, hash);
}
Arquivo lib/auth/session.ts. O Map é só para demo. Em produção, use Redis, PostgreSQL, DynamoDB ou outro store compartilhado.
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import { z } from "zod";
const env = z
.object({
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
SESSION_SECRET: z.string().min(32),
})
.parse(process.env);
export type Role = "user" | "admin";
type SessionRecord = { userId: string; role: Role; csrfToken: string; expiresAt: number };
declare global {
var demoSessions: Map<string, SessionRecord> | undefined;
}
const sessions = globalThis.demoSessions ?? new Map<string, SessionRecord>();
globalThis.demoSessions = sessions;
export const SESSION_MAX_AGE_SECONDS = 60 * 60 * 8;
export const SESSION_COOKIE_NAME =
env.NODE_ENV === "production" ? "__Host-session" : "dev-session";
export const sessionCookieOptions = {
httpOnly: true,
secure: env.NODE_ENV === "production",
sameSite: "lax" as const,
path: "/",
maxAge: SESSION_MAX_AGE_SECONDS,
};
function signSessionId(sessionId: string) {
return createHmac("sha256", env.SESSION_SECRET).update(sessionId).digest("base64url");
}
function safeEqual(left: string, right: string) {
const a = Buffer.from(left);
const b = Buffer.from(right);
return a.length === b.length && timingSafeEqual(a, b);
}
export function createSession(userId: string, role: Role = "user") {
const sessionId = randomBytes(32).toString("base64url");
const token = `${sessionId}.${signSessionId(sessionId)}`;
const csrfToken = randomBytes(32).toString("base64url");
sessions.set(sessionId, {
userId,
role,
csrfToken,
expiresAt: Date.now() + SESSION_MAX_AGE_SECONDS * 1000,
});
return { token, csrfToken };
}
export function getSession(token?: string) {
if (!token) return null;
const [sessionId, signature] = token.split(".");
if (!sessionId || !signature || !safeEqual(signature, signSessionId(sessionId))) return null;
const session = sessions.get(sessionId);
if (!session || session.expiresAt < Date.now()) {
sessions.delete(sessionId);
return null;
}
return { id: sessionId, ...session };
}
export function destroySession(token?: string) {
const sessionId = token?.split(".")[0];
if (sessionId) sessions.delete(sessionId);
}
export function assertSameOrigin(request: Request) {
const origin = request.headers.get("origin");
if (origin && origin !== new URL(request.url).origin) throw new Error("Bad origin");
}
export function assertCsrf(request: Request, session: { csrfToken: string }) {
const submitted = request.headers.get("x-csrf-token");
if (!submitted || submitted !== session.csrfToken) throw new Error("Bad CSRF token");
}
Route Handler app/api/login/route.ts. Em produto real, busque passwordHash e role no banco.
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { hashPassword, verifyPassword } from "@/lib/auth/password";
import { SESSION_COOKIE_NAME, createSession, sessionCookieOptions } from "@/lib/auth/session";
export const runtime = "nodejs";
export const loginInputSchema = z.object({
email: z.string().trim().toLowerCase().email(),
password: z.string().min(12).max(128),
});
async function findUserByEmail(email: string) {
if (email !== "[email protected]") return null;
return {
id: "user_123",
role: "admin" as const,
passwordHash: await hashPassword("correct-horse-battery-staple"),
};
}
export async function POST(request: NextRequest) {
const parsed = loginInputSchema.safeParse(await request.json());
if (!parsed.success) return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
const user = await findUserByEmail(parsed.data.email);
const passwordOk = user ? await verifyPassword(parsed.data.password, user.passwordHash) : false;
if (!user || !passwordOk) {
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
}
const session = createSession(user.id, user.role);
const response = NextResponse.json({ ok: true, csrfToken: session.csrfToken });
response.cookies.set({ name: SESSION_COOKIE_NAME, value: session.token, ...sessionCookieOptions });
response.cookies.set({
name: "csrf-token",
value: session.csrfToken,
secure: sessionCookieOptions.secure,
sameSite: "lax",
path: "/",
maxAge: sessionCookieOptions.maxAge,
});
return response;
}
middleware.ts só redireciona. A autorização real continua nos handlers do servidor.
import { NextRequest, NextResponse } from "next/server";
const SESSION_COOKIE_NAME =
process.env.NODE_ENV === "production" ? "__Host-session" : "dev-session";
export function middleware(request: NextRequest) {
const hasSession = request.cookies.has(SESSION_COOKIE_NAME);
const pathname = request.nextUrl.pathname;
if (!hasSession && (pathname.startsWith("/dashboard") || pathname.startsWith("/admin"))) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = { matcher: ["/dashboard/:path*", "/admin/:path*"] };
Teste test/auth.test.ts.
import { beforeAll, describe, expect, it } from "vitest";
beforeAll(() => {
process.env.NODE_ENV = "test";
process.env.SESSION_SECRET = "test-secret-value-with-more-than-32-characters";
});
describe("auth primitives", () => {
it("hashes and verifies passwords", async () => {
const { hashPassword, verifyPassword } = await import("../lib/auth/password");
const hash = await hashPassword("correct-horse-battery-staple");
await expect(verifyPassword("correct-horse-battery-staple", hash)).resolves.toBe(true);
await expect(verifyPassword("wrong-password", hash)).resolves.toBe(false);
});
it("creates and destroys a session", async () => {
const { createSession, destroySession, getSession } = await import("../lib/auth/session");
const session = createSession("user_123", "admin");
expect(getSession(session.token)?.role).toBe("admin");
destroySession(session.token);
expect(getSession(session.token)).toBeNull();
});
it("validates login input", async () => {
const { loginInputSchema } = await import("../app/api/login/route");
expect(loginInputSchema.safeParse({ email: "bad", password: "short" }).success).toBe(false);
});
});
Password reset, OAuth e JWT
Recuperação de senha não pode revelar se um e-mail existe. Retorne a mesma mensagem, gere token com alta entropia, salve apenas o hash, expire rápido e permita uso único. Depois da troca de senha, ofereça invalidar sessões existentes.
Para OAuth, prefira Auth.js ou biblioteca madura. O provedor autentica; sua aplicação vincula a identidade a um usuário local, cria sua sessão, valida roles e registra eventos. Não transforme access token do provedor em cookie de sessão da sua aplicação.
JWT é útil para APIs, mas não resolve tudo. Defina aud, iss, exp, rotação de chaves e resposta a vazamento.
Pitfalls, auditoria e monetização
Bloqueie estes erros antes do deploy: JWT longo em localStorage, cookies sem Secure ou HttpOnly, SHA-256 simples para senha, reset token em texto puro, erro de login que revela se o e-mail existe, RBAC só no middleware e logs com secrets.
Audit log deve registrar actor, action, result e horário. Não registre senha, session ID, reset token ou OAuth access token. Em SaaS, valide tenantId antes de role.
Autenticação também protege receita: conteúdo premium, templates, links Gumroad, formulários B2B e dashboards de membros. Comece pela checklist gratuita, use produtos e templates quando precisar de material reutilizável e procure treinamento ou consultoria Claude Code para desenhar auth, RBAC, logs e CI em um repositório real.
Resultado prático
Quando Masa testou esse fluxo, o ganho não foi só a rota de login. O valor veio de tratar session cookie, CSRF, RBAC, audit log e testes como uma unidade. Antes, o middleware parecia fronteira de segurança, mas só verificava presença de cookie. Colocar as validações reais no servidor deixou o review mais claro.
Resumo
Com Claude Code, comece pelas fronteiras: navegador usa sessão no servidor e cookie seguro; API usa JWT curto; login externo usa OAuth/OIDC com biblioteca; mudança de estado usa CSRF e Origin; autorização usa RBAC; ações sensíveis usam auditoria e testes.
Artigos relacionados
Implementar i18n com Claude Code: guia prático para Next.js
Fluxo prático para i18n em Next.js com Claude Code, next-intl, checagem de traduções, casos de uso e armadilhas.
Como implementar paginação com Claude Code, React e Next.js
Guia prático de paginação com Claude Code: estado na URL, API, acessibilidade e testes de borda.
Gerenciamento seguro de cookies com Claude Code: sessões Next.js, CSRF e consentimento
Implemente cookies seguros no Next.js com Claude Code: HttpOnly, Secure, SameSite, CSRF, logout e consentimento.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.