Use Cases (Atualizado: 02/06/2026)

OAuth com Claude Code: PKCE, state, nonce e armazenamento seguro de tokens

Guia prático de OAuth 2.1/2.0 Authorization Code + PKCE com Claude Code, cobrindo state, nonce e tokens.

OAuth com Claude Code: PKCE, state, nonce e armazenamento seguro de tokens

OAuth não termina quando o botão “Entrar com Google” aparece. Um detalhe errado pode vincular a conta errada, reutilizar um código de autorização, trocar o redirect URI ou expor tokens ao JavaScript do navegador. Este guia mostra como usar Claude Code para criar a base sem entregar segredos reais e como revisar a implementação.

O padrão prático é OAuth 2.0 Authorization Code + PKCE, alinhado com OAuth 2.1. PKCE permite que o app prove, no endpoint de token, que ainda possui o verifier original ligado ao challenge enviado no início. Para contexto, veja autenticação com Claude Code, desenvolvimento de API e primeiros passos.

Estrutura recomendada

Use Claude Code para rotas, sessões, testes e checklists. Não cole client secrets, refresh tokens, .env de produção ou capturas da console do provedor. Passe nomes de variáveis, comportamento esperado e casos de falha.

ÁreaRecomendaçãoMotivo
FluxoAuthorization Code + PKCEEvita exposição de tokens em fluxos antigos
CSRFSalvar e validar stateBloqueia callback falsificado
Replay OIDCSalvar e validar nonceDetecta identidade reutilizada
Redirect URICorrespondência exataImpede troca de destino
TokensSessão no servidor ou store criptografadoMantém tokens longos fora do localStorage
Entrada no Claude CodeValores fictícios e especificaçãoEvita segredos em prompts e logs

Fontes primárias: OAuth 2.1, RFC 7636 PKCE, RFC 9700 OAuth 2.0 Security BCP, OpenID Connect Core, OWASP OAuth2 Cheat Sheet, Claude Code Security.

Três casos reais

O primeiro é um painel B2B SaaS. Usuários entram com Google Workspace ou Microsoft Entra ID, mas o app ainda mapeia usuário, organização e papel internos. OAuth prova identidade; sua autorização decide permissões.

O segundo é acesso delegado a APIs. Calendário, e-mail, armazenamento e CRM exigem consentimento, scopes mínimos, refresh tokens criptografados e fluxo de desconexão.

O terceiro são ferramentas internas e serviços no estilo MCP. API keys estáticas são simples no início, mas dificultam revogação e auditoria. OAuth/OIDC separa login, consentimento, expiração e identidade.

Apps móveis e SPAs não escondem client secret, então PKCE é obrigatório. Mesmo em apps web com servidor, PKCE facilita revisão.

Demo local copiável

A demo não precisa de Google, Microsoft ou Auth0. Um processo Express atua como cliente OAuth e servidor de autorização simulado, mostrando state, nonce, PKCE S256, redirect URI exato, códigos de uso único e tokens em sessão do servidor.

Crie os arquivos em uma pasta vazia, rode npm install && npm start e abra http://localhost:3000.

{
  "scripts": { "start": "node server.mjs" },
  "dependencies": { "express": "^4.19.2", "express-session": "^1.18.0" },
  "engines": { "node": ">=20" }
}
// server.mjs
import crypto from "node:crypto";
import express from "express";
import session from "express-session";

const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(session({
  name: "oauth_demo_sid",
  secret: "dev-only-change-this-32-byte-secret",
  resave: false,
  saveUninitialized: false,
  cookie: { httpOnly: true, sameSite: "lax", secure: false, maxAge: 10 * 60 * 1000 },
}));

const client = {
  clientId: "claude-code-demo",
  redirectUri: "http://localhost:3000/callback",
  scope: "openid profile email",
};
const authorizationEndpoint = "http://localhost:3000/mock/authorize";
const tokenEndpoint = "http://localhost:3000/mock/token";
const registeredRedirectUris = new Set([client.redirectUri]);
const pendingCodes = new Map();

function randomUrlSafe(bytes = 32) {
  return crypto.randomBytes(bytes).toString("base64url");
}
function sha256Base64Url(value) {
  return crypto.createHash("sha256").update(value).digest("base64url");
}
function fail(res, status, message) {
  return res.status(status).type("text/plain").send(message);
}

app.get("/", (_req, res) => {
  res.type("html").send(`<h1>OAuth PKCE local demo</h1><p><a href="/auth/login">Start login</a></p>`);
});

app.get("/auth/login", (req, res) => {
  const state = randomUrlSafe();
  const nonce = randomUrlSafe();
  const codeVerifier = randomUrlSafe(48);
  const codeChallenge = sha256Base64Url(codeVerifier);
  req.session.oauth = { state, nonce, codeVerifier, createdAt: Date.now() };

  const params = new URLSearchParams({
    response_type: "code",
    client_id: client.clientId,
    redirect_uri: client.redirectUri,
    scope: client.scope,
    state,
    nonce,
    code_challenge: codeChallenge,
    code_challenge_method: "S256",
  });
  res.redirect(`${authorizationEndpoint}?${params}`);
});

app.get("/mock/authorize", (req, res) => {
  const p = req.query;
  const redirectUri = String(p.redirect_uri || "");
  if (p.response_type !== "code") return fail(res, 400, "response_type must be code");
  if (p.client_id !== client.clientId) return fail(res, 400, "unknown client_id");
  if (!registeredRedirectUris.has(redirectUri)) return fail(res, 400, "redirect_uri is not registered exactly");
  if (p.code_challenge_method !== "S256") return fail(res, 400, "PKCE S256 is required");
  if (!p.code_challenge || !p.state || !p.nonce) return fail(res, 400, "missing state, nonce, or PKCE challenge");

  const code = randomUrlSafe(24);
  pendingCodes.set(code, {
    clientId: client.clientId,
    redirectUri,
    codeChallenge: String(p.code_challenge),
    nonce: String(p.nonce),
    expiresAt: Date.now() + 60_000,
    used: false,
  });

  const redirect = new URL(redirectUri);
  redirect.searchParams.set("code", code);
  redirect.searchParams.set("state", String(p.state));
  res.redirect(redirect.toString());
});

app.get("/callback", async (req, res) => {
  const oauth = req.session.oauth;
  const code = String(req.query.code || "");
  const returnedState = String(req.query.state || "");
  if (!oauth) return fail(res, 400, "missing OAuth session");
  if (returnedState !== oauth.state) return fail(res, 403, "state mismatch: possible CSRF or mixed login attempt");

  const response = await fetch(tokenEndpoint, {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: client.redirectUri,
      client_id: client.clientId,
      code_verifier: oauth.codeVerifier,
    }),
  });
  const tokens = await response.json();
  if (!response.ok) return fail(res, response.status, JSON.stringify(tokens, null, 2));
  if (tokens.nonce !== oauth.nonce) return fail(res, 403, "nonce mismatch: possible replay");

  req.session.oauth = undefined;
  req.session.tokenSet = {
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token,
    expiresAt: Date.now() + tokens.expires_in * 1000,
  };
  res.redirect("/dashboard");
});

app.post("/mock/token", (req, res) => {
  const body = req.body;
  const record = pendingCodes.get(body.code);
  if (body.grant_type !== "authorization_code") return res.status(400).json({ error: "unsupported_grant_type" });
  if (!record || record.used || record.expiresAt < Date.now()) return res.status(400).json({ error: "invalid_grant" });
  if (body.client_id !== record.clientId) return res.status(400).json({ error: "invalid_client" });
  if (body.redirect_uri !== record.redirectUri) return res.status(400).json({ error: "invalid_redirect_uri" });
  if (sha256Base64Url(body.code_verifier || "") !== record.codeChallenge) return res.status(400).json({ error: "invalid_code_verifier" });

  record.used = true;
  res.json({ token_type: "Bearer", access_token: randomUrlSafe(32), refresh_token: randomUrlSafe(32), expires_in: 300, nonce: record.nonce });
});

app.get("/dashboard", (req, res) => {
  const tokenSet = req.session.tokenSet;
  if (!tokenSet) return res.redirect("/auth/login");
  const secondsLeft = Math.max(0, Math.floor((tokenSet.expiresAt - Date.now()) / 1000));
  res.type("html").send(`<h1>Logged in</h1><p>Access token is stored server-side, not in localStorage.</p><p>Expires in ${secondsLeft} seconds.</p>`);
});

app.listen(3000, () => console.log("Open http://localhost:3000"));

Observe: /auth/login guarda state, nonce e code_verifier na sessão; a requisição envia só code_challenge; /callback valida state; /mock/token recalcula S256; /dashboard lê tokens do servidor, não do localStorage.

Prompt para Claude Code

Implemente OAuth 2.0 Authorization Code + PKCE em Express + TypeScript.
Requisitos:
- Definir configuração do provedor apenas como nomes de variáveis de ambiente; sem segredos reais.
- Guardar state, nonce e code_verifier em sessão no servidor.
- Validar redirect_uri por correspondência exata.
- Permitir somente code_challenge_method=S256.
- Nunca salvar access token ou refresh token em localStorage.
- Salvar refresh tokens apenas em campo criptografado ou sessão no servidor.
- Adicionar testes de sucesso, state inválido, PKCE inválido, código expirado e reuso.
- Finalizar com checklist de segurança.

Para revisar:

Revise esta implementação OAuth contra RFC 9700, RFC 7636 e OWASP OAuth2 Cheat Sheet.
Verifique segredos em logs, snapshots, bundles frontend e Git diff.
Classifique achados como High, Medium ou Low e proponha patches.

Armadilhas comuns

Não valide redirect URI por prefixo. https://app.example.com.evil.test/callback não é seu app. Use correspondência exata.

Não gere state sem validar. Salve no início, compare no callback e remova depois. Para várias abas, guarde transações curtas por state.

Não permita PKCE plain sem motivo forte. Prefira S256, não registre o verifier, expire códigos rapidamente e use uma única vez.

Com OIDC, nonce não basta. Valide assinatura JWT, issuer, audience, expiração e nonce. ID Token é identidade; Access Token é autorização de API.

Evite localStorage para tokens longos. Use sessões no servidor, tabelas criptografadas, cookies curtos e revogação. Veja Claude Code cookie management.

Checklist de entrega profissional

  • Tempo de vida dos tokens e refresh documentados.
  • O time explica state, nonce e PKCE com palavras simples.
  • Redirect URIs, scopes e configurações do provedor registrados.
  • Logs sem code, verifier, token ou cookie.
  • Erros não expõem detalhes internos.
  • Testes cobrem sucesso, mismatch, expiração e reuso.
  • Saída do Claude Code revisada contra fontes oficiais.

Treinamento e consultoria ClaudeCodeLab podem transformar isso em workshop: trazer um fluxo OAuth real, mapear riscos, adicionar testes e sair com checklist reutilizável. Veja ClaudeCodeLab training.

Verificação prática

A demo percorre início do login, autorização simulada, callback, troca de token e dashboard. Alterar state gera state mismatch; verifier errado retorna invalid_code_verifier. Em protótipos OAuth, os problemas que escapam costumam ser múltiplas abas, botão voltar, códigos expirados e logs acidentais. Inclua esses casos no primeiro prompt ao Claude Code.

Resumo

A qualidade de OAuth está em proteger a transação inteira. Use Authorization Code + PKCE, valide state e nonce, compare redirect URI exatamente e mantenha tokens no servidor. Claude Code acelera, mas precisa de restrições explícitas e revisão por fontes primárias.

#Claude Code #OAuth #authentication #security #TypeScript
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.