Use Cases (Atualizado: 02/06/2026)

Autenticação JWT com Claude Code: claims, cookies, rotação e chaves

Implemente JWT com Claude Code: claims, cookies, rotação de refresh tokens, revogação, chaves e prompts seguros.

Autenticação JWT com Claude Code: claims, cookies, rotação e chaves

JWT parece simples em um exemplo curto: assine um payload, retorne um token e valide no middleware. Em produção, a história muda. Um aud sem validação, um refresh token longo demais, token em localStorage ou uma rotação de chaves mal planejada pode virar uma falha séria de conta.

Este guia explica JWT para iniciantes e transforma o assunto em uma especificação prática para Claude Code. Vamos cobrir design de claims, assinatura versus criptografia, localização em Cookie ou sessão, refresh-token rotation, revogação, rotação de chaves, falhas comuns e prompts seguros. Para o fluxo de login completo, leia também implementação de autenticação, gestão de cookies e RBAC com Claude Code.

Use fontes primárias ao adaptar o código: RFC 7519 para JWT, RFC 8725 para práticas seguras, RFC 9700 para refresh tokens e replay, OWASP JWT Cheat Sheet, MDN Set-Cookie, jose e Claude Code settings.

Fundamentos de JWT para iniciantes

Um JWT tem três partes separadas por pontos: header.payload.signature. O header informa tipo e algoritmo. O payload contém claims, ou seja, afirmações sobre o token. A assinatura detecta adulteração. Claims comuns são sub para usuário, iss para emissor, aud para audiência, exp para expiração e jti para ID do token.

O ponto mais importante é que um JWT comum é assinado, não criptografado. A assinatura não esconde o payload. Quem obtiver o token consegue decodificar seu conteúdo. Não coloque API keys, endereço, dados de pagamento, notas internas ou informações pessoais desnecessárias no payload. Se algo precisa ser secreto, mantenha no servidor e deixe o JWT carregar apenas referências mínimas.

Comece passando esse modelo para Claude Code.

Implemente autenticação JWT.
Regras:
- O payload JWT é assinado, não criptografado. Não colocar secrets.
- Access token com no máximo 15 minutos.
- Refresh token com no máximo 7 dias.
- Validar iss, aud, sub, exp, iat e jti.
- Recusar alg none e algoritmos inesperados.
- Implementar refresh-token rotation e detecção de reutilização.
- Revisar Cookie, CSRF, XSS, revogação e rotação de chaves.

Projete claims mínimos

JWT não deve ser cache do perfil do usuário. O access token deve carregar apenas o mínimo necessário para a entrada da API: ID estável do usuário, session id, tenant id, role amplo e token id. Plano, bloqueio, permissões finas e limites de uso mudam com frequência; confirme esses dados no servidor.

ClaimUsoCuidado
subID do usuárioPrefira ID interno a email
issEmissorFixe no servidor de autenticação
audAudiênciaRecuse tokens de outra API
expExpiraçãoAccess token curto
jtiID do tokenRevogação e auditoria
sidID de sessãoLogout por dispositivo e token family
rolePapel amploRevalide permissões finas no servidor

O erro clássico é colocar plan: "pro" oudisabled: false no token. Quando o plano muda ou a conta é suspensa, o token continua com o estado antigo até expirar. Autenticação responde “quem é?”. Autorização responde “pode fazer isso agora?”. Não misture tudo no payload.

TypeScript executável com jose

O demo abaixo assina e verifica access tokens, guarda apenas hashes de refresh tokens, rotaciona refresh tokens e detecta reutilização. Em produção, substitua Map por Redis ou banco de dados, adicione rate limit, auditoria, HTTPS e proteção CSRF.

mkdir jwt-lab
cd jwt-lab
npm init -y
npm install jose
npm install -D tsx typescript @types/node
// auth-demo.ts
import { createHash, createSecretKey, randomUUID } from "node:crypto";
import { SignJWT, jwtVerify } from "jose";

const ISSUER = "https://auth.example.com";
const AUDIENCE = "claudecodelab-api";
const ACCESS_TTL = "15m";
const REFRESH_TTL_SECONDS = 60 * 60 * 24 * 7;

const accessKey = createSecretKey(
  Buffer.from(
    process.env.JWT_ACCESS_SECRET ??
      "dev-only-secret-change-me-32-bytes-minimum"
  )
);

const refreshKey = createSecretKey(
  Buffer.from(
    process.env.JWT_REFRESH_SECRET ??
      "dev-only-refresh-secret-change-me-32-bytes"
  )
);

type Role = "admin" | "user" | "viewer";
type User = { id: string; role: Role; tenantId: string };
type VerifiedAccess = {
  userId: string;
  role: Role;
  tenantId: string;
  sessionId: string;
  tokenId: string;
};

type RefreshRecord = {
  userId: string;
  sessionId: string;
  tokenHash: string;
  expiresAt: number;
  revokedAt?: number;
};

const refreshStore = new Map<string, RefreshRecord>();
const revokedAccessTokenIds = new Set<string>();

function sha256(value: string) {
  return createHash("sha256").update(value).digest("hex");
}

function assertRole(value: unknown): asserts value is Role {
  if (!["admin", "user", "viewer"].includes(String(value))) {
    throw new Error("invalid role claim");
  }
}

async function signAccessToken(user: User, sessionId: string) {
  const tokenId = randomUUID();

  return new SignJWT({ role: user.role, tid: user.tenantId, sid: sessionId })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setIssuer(ISSUER)
    .setAudience(AUDIENCE)
    .setSubject(user.id)
    .setIssuedAt()
    .setExpirationTime(ACCESS_TTL)
    .setJti(tokenId)
    .sign(accessKey);
}

async function verifyAccessToken(token: string): Promise<VerifiedAccess> {
  const { payload } = await jwtVerify(token, accessKey, {
    issuer: ISSUER,
    audience: AUDIENCE,
    algorithms: ["HS256"],
  });

  assertRole(payload.role);

  if (
    typeof payload.sub !== "string" ||
    typeof payload.tid !== "string" ||
    typeof payload.sid !== "string" ||
    typeof payload.jti !== "string"
  ) {
    throw new Error("missing required claim");
  }

  if (revokedAccessTokenIds.has(payload.jti)) {
    throw new Error("access token revoked");
  }

  return {
    userId: payload.sub,
    role: payload.role,
    tenantId: payload.tid,
    sessionId: payload.sid,
    tokenId: payload.jti,
  };
}

async function signRefreshToken(user: User, sessionId: string) {
  const tokenId = randomUUID();
  const token = await new SignJWT({ sid: sessionId, kind: "refresh" })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setIssuer(ISSUER)
    .setAudience("claudecodelab-refresh")
    .setSubject(user.id)
    .setIssuedAt()
    .setExpirationTime("7d")
    .setJti(tokenId)
    .sign(refreshKey);

  refreshStore.set(tokenId, {
    userId: user.id,
    sessionId,
    tokenHash: sha256(token),
    expiresAt: Date.now() + REFRESH_TTL_SECONDS * 1000,
  });

  return token;
}

async function rotateRefreshToken(refreshToken: string, user: User) {
  const { payload } = await jwtVerify(refreshToken, refreshKey, {
    issuer: ISSUER,
    audience: "claudecodelab-refresh",
    algorithms: ["HS256"],
  });

  if (
    typeof payload.jti !== "string" ||
    typeof payload.sid !== "string" ||
    typeof payload.sub !== "string"
  ) {
    throw new Error("invalid refresh token claims");
  }

  const record = refreshStore.get(payload.jti);
  const presentedHash = sha256(refreshToken);

  if (!record || record.revokedAt || record.tokenHash !== presentedHash) {
    for (const item of refreshStore.values()) {
      if (item.sessionId === payload.sid) item.revokedAt = Date.now();
    }
    throw new Error("refresh token reuse detected");
  }

  if (record.expiresAt < Date.now()) {
    throw new Error("refresh token expired");
  }

  record.revokedAt = Date.now();

  return {
    accessToken: await signAccessToken(user, payload.sid),
    refreshToken: await signRefreshToken(user, payload.sid),
  };
}

async function main() {
  const user: User = {
    id: "user_123",
    role: "admin",
    tenantId: "tenant_a",
  };
  const sessionId = randomUUID();
  const accessToken = await signAccessToken(user, sessionId);
  const refreshToken = await signRefreshToken(user, sessionId);
  const verified = await verifyAccessToken(accessToken);
  const rotated = await rotateRefreshToken(refreshToken, user);

  console.log({ verified, rotatedRefreshLength: rotated.refreshToken.length });
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
npx tsx auth-demo.ts

O ponto central é não guardar o refresh token em texto puro. O armazenamento recebe apenas o hash. Quando o token é usado, o registro antigo é revogado e um novo par é emitido. Se o token antigo aparecer de novo, toda a família daquele sid é revogada.

Cookies, sessão e armazenamento

Em aplicações web, o refresh token normalmente fica em uma Cookie HttpOnly, Secure e SameSite. HttpOnly impede que JavaScript leia o valor diretamente, reduzindo roubo em caso de XSS. Mas Cookies são enviados automaticamente, então rotas de refresh, logout e mudança de estado ainda precisam de defesa CSRF.

const refreshCookieOptions = {
  httpOnly: true,
  secure: true,
  sameSite: "lax" as const,
  path: "/api/auth/refresh",
  maxAge: 60 * 60 * 24 * 7,
};

const clearRefreshCookieOptions = {
  ...refreshCookieOptions,
  maxAge: 0,
};

O access token pode ficar em memória em uma SPA, aceitando que um reload exige refresh. Com BFF ou Route Handlers do Next.js, o servidor pode chamar a API sem expor o access token ao navegador. localStorage é prático, mas qualquer XSS pode ler bearer tokens. Evite tokens longos ali.

Revogação e rotação de chaves

JWTs continuam válidos até exp se o servidor não adicionar controles. Combine access tokens curtos, lista de jti revogados para eventos críticos, revogação por sid e refresh-token rotation. Logout revoga o refresh record. Troca de senha, suspensão e suspeita de vazamento revogam todas as sessões da conta.

Rotação de chaves precisa de sobreposição. HS256 é simples, mas todo verificador conhece o secret compartilhado. Com vários serviços, RS256 ou ES256 com JWKS costuma ser melhor: verificadores só precisam de chaves públicas.

import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(
  new URL("https://auth.example.com/.well-known/jwks.json")
);

export async function verifyWithRotatingKeys(token: string) {
  return jwtVerify(token, JWKS, {
    issuer: "https://auth.example.com",
    audience: "claudecodelab-api",
    algorithms: ["RS256", "ES256"],
  });
}
{
  "rotationPlan": {
    "step1": "Gerar nova chave e publicar no JWKS",
    "step2": "Assinar novos tokens com o novo kid",
    "step3": "Manter a chave pública antiga até os tokens expirarem",
    "step4": "Revisar logs e remover a chave antiga"
  }
}

Casos de uso e prompt seguro

flowchart LR
  Login["Login"] --> Access["Access token curto"]
  Login --> Refresh["Refresh cookie HttpOnly"]
  Access --> API["API valida iss/aud/exp/jti"]
  Refresh --> Rotate["Rotação no refresh"]
  Rotate --> Store["DB/Redis guarda hash e sid"]
  Store --> Reuse["Reuse: revogar família"]

Caso 1: painel SaaS. tenantId pode entrar no claim para contexto, mas consultas no banco também precisam filtrar por tenant. Permissões administrativas, plano e suspensão devem ser rechecados em operações destrutivas.

Caso 2: conteúdo pago ou cursos. Mantenha access tokens curtos e refresh silencioso para não interromper o aluno. Se o site também tem anúncios, Analytics e CTA de compra, combine autenticação com headers de segurança web e consentimento de cookies.

Caso 3: aplicativos móveis ou desktop. Use armazenamento seguro do sistema operacional em vez de Cookie do navegador. Mantenha sid para revogar dispositivo perdido e registre reutilização de refresh token como evento de segurança.

Caso 4: microserviços. Não distribua secret simétrico para todos os serviços. Considere verificação por chave pública, gateway ou token exchange. Cada serviço valida aud.

Projete e implemente autenticação JWT neste repositório.
Antes de editar, gere uma tabela com:
- framework, modelo de usuário, código session/cookie, middleware auth
- checks de autorização, CSRF, CSP e rate limit existentes
- local atual dos tokens e risco XSS/CSRF

Regras:
- Usar jose. Não voltar para jsonwebtoken.
- Access token 15 minutos. Refresh token 7 dias.
- Validar iss, aud, sub, exp, iat, jti e sid.
- Guardar só hash do refresh token em DB/Redis.
- Rotacionar refresh tokens e revogar sid family no reuse.
- Não imprimir secrets, .env ou tokens de produção.
- Terminar com evidência de teste ou curl.

Falhas, verificação e CTA

Falhas frequentes: não fixar algoritmo, colocar dados sensíveis no payload, não validar aud ouiss, permitir reuse de refresh token, fazer logout só no navegador, remover chave antiga cedo demais e colar secrets reais no Claude Code. As defesas são allowlist de algoritmos, claims mínimos, rotation, revogação por sid, janela JWKS e prompts sem secrets.

curl -i -X POST https://example.com/api/auth/login \
  -H "content-type: application/json" \
  -d '{"email":"demo@example.com","password":"correct horse"}'

npm test -- --runInBand auth

Teste token expirado, assinatura alterada, audience errada, jti revogado, refresh token reutilizado, logout, troca de senha e suspensão. Confirme também que a Cookie de refresh tenha HttpOnly, Secure, SameSite adequado e path restrito.

Para padronizar esse fluxo, comece pela cheatsheet gratuita de Claude Code e fixe hábitos de verificação. Para prompts e modelos reutilizáveis, veja ClaudeCodeLab products. Para equipes que precisam alinhar JWT, RBAC, cookies, logs de auditoria e CI gates, use Claude Code training and consultation.

Ao testar o exemplo deste artigo, a maior melhoria veio de escrever a tabela de claims antes do código de assinatura. Isso revelou cedo três problemas: aud ausente, um exemplo com top-level await que não rodava no caminho padrão do tsx e o risco de salvar refresh tokens em texto puro. JWT seguro não é só assinar token; é juntar claims, validação, armazenamento, rotação, revogação e chaves em um fluxo revisável.

#Claude Code #JWT #autenticação #segurança #Node.js
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.