Use Cases (Atualizado: 02/06/2026)

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 segura com Claude Code: sessões Next.js, JWT e OAuth

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.

MecanismoMelhor usoForçaRisco a controlar
Sessão no servidorDashboard SaaS, área de membros, adminRevogação e auditoria simplesPrecisa de Redis, Postgres, DynamoDB ou outro store
JWTAPI móvel, chamadas curtas entre serviços, API externaVerificação sem consulta ao bancoRevogação difícil; evite armazenamento longo no browser
OAuth / OIDCGoogle, GitHub, SSO corporativoVocê não guarda a senha do usuárioLogin 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.

#Claude Code #authentication #Next.js #JWT #OAuth #security
Grátis

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.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.