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 !== "masa@example.com") 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.
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.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.