Use Cases (Actualizado: 2/6/2026)

OAuth con Claude Code: PKCE, state, nonce y almacenamiento seguro de tokens

Guía práctica para implementar OAuth 2.1/2.0 Authorization Code + PKCE con Claude Code sin filtrar secretos.

OAuth con Claude Code: PKCE, state, nonce y almacenamiento seguro de tokens

OAuth no termina cuando aparece el botón “Iniciar sesión con Google”. Un detalle mal implementado puede vincular la cuenta equivocada, reutilizar un código de autorización, cambiar el redirect URI o exponer tokens a JavaScript del navegador. Esta guía muestra cómo pedir a Claude Code que genere una base segura sin entregarle secretos reales, y cómo revisar el resultado.

El valor por defecto práctico es OAuth 2.0 Authorization Code + PKCE, alineado con OAuth 2.1. PKCE permite que la app demuestre en el endpoint de tokens que conserva el verifier original asociado al challenge enviado al iniciar el login. Para contexto, revisa Claude Code para autenticación, desarrollo de APIs y primeros pasos.

Forma recomendada

Usa Claude Code para rutas, sesiones, pruebas y listas de revisión. No pegues client secrets, refresh tokens, .env de producción ni capturas de la consola del proveedor. Entrega nombres de variables, comportamiento esperado y casos de fallo.

ÁreaRecomendaciónMotivo
FlujoAuthorization Code + PKCEEvita exposición de tokens de flujos antiguos
CSRFGuardar y validar stateBloquea callbacks falsificados
Repetición OIDCGuardar y validar nonceDetecta resultados de identidad reutilizados
Redirect URICoincidencia exactaEvita sustitución de destino
TokensSesión de servidor o almacenamiento cifradoMantiene tokens largos fuera de localStorage
Entrada a Claude CodeValores ficticios y especificaciónEvita fugas en prompts y logs

Referencias primarias: OAuth 2.1, RFC 7636 PKCE, RFC 9700 OAuth 2.0 Security BCP, OpenID Connect Core, OWASP OAuth2 Cheat Sheet y Claude Code Security.

Tres casos reales

El primero es un panel B2B SaaS. El usuario entra con Google Workspace o Microsoft Entra ID, pero tu app lo mapea a usuario, organización y rol propios. OAuth prueba la identidad; tu capa de autorización decide permisos.

El segundo es acceso delegado a APIs. Calendario, correo, almacenamiento y CRM requieren consentimiento, scopes mínimos, refresh tokens cifrados y una opción clara para desconectar.

El tercero son herramientas internas y servicios tipo MCP. Las API keys estáticas son fáciles al principio, pero complican revocación y auditoría. OAuth/OIDC separa mejor login, consentimiento, expiración e identidad.

En apps móviles y SPA, PKCE es obligatorio porque no pueden ocultar un client secret. Incluso en apps web con servidor, usar PKCE facilita la revisión.

Demo local copiable

Esta demo no necesita credenciales de Google, Microsoft ni Auth0. Un proceso Express actúa como cliente OAuth y servidor de autorización simulado para observar state, nonce, PKCE S256, redirect URI exacto, códigos de un solo uso y tokens en sesión de servidor.

Crea estos archivos en una carpeta vacía, ejecuta npm install && npm start y abre 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"));

Fíjate en que /auth/login guarda state, nonce y code_verifier en la sesión; la solicitud solo envía code_challenge; /callback valida state; /mock/token recalcula S256; y /dashboard lee tokens desde el servidor, no desde localStorage.

Prompt para Claude Code

Implementa OAuth 2.0 Authorization Code + PKCE en Express + TypeScript.
Requisitos:
- Define la configuración del proveedor solo como nombres de variables de entorno; no incluyas secretos reales.
- Guarda state, nonce y code_verifier en una sesión del servidor.
- Valida redirect_uri con coincidencia exacta del valor configurado.
- Permite solo code_challenge_method=S256.
- No guardes access tokens ni refresh tokens en localStorage.
- Guarda refresh tokens solo en un campo cifrado de base de datos o en sesión de servidor.
- Agrega pruebas para éxito, state inválido, PKCE inválido, código expirado y reutilización de código.
- Termina con una lista de revisión de seguridad.

Para revisión:

Revisa esta implementación OAuth según RFC 9700, RFC 7636 y OWASP OAuth2 Cheat Sheet.
Comprueba si hay secretos en logs, snapshots, bundles frontend o Git diff.
Clasifica hallazgos como High, Medium o Low y propone parches.

Errores concretos

No valides redirect URI por prefijo. https://app.example.com.evil.test/callback no es tu aplicación. Usa coincidencia exacta.

No generes state sin comprobarlo. Guárdalo al iniciar login, compáralo en callback y elimínalo después. Para varias pestañas, guarda transacciones cortas por state.

No permitas PKCE plain salvo una razón de compatibilidad muy clara. Prefiere S256, no registres el verifier, expira los códigos rápido y hazlos de un solo uso.

Con OIDC, nonce no basta. Valida firma JWT, issuer, audience, expiración y nonce. ID Token prueba identidad; Access Token autoriza APIs. Mezclarlos crea errores sutiles.

Evita localStorage para tokens de larga duración. En web tradicional, usa sesiones de servidor, tablas cifradas, cookies cortas y revocación. Para cookies, consulta Claude Code cookie management.

Checklist para entrega comercial

  • Vidas de tokens y refresh están documentados.
  • El equipo explica state, nonce y PKCE con sus propias palabras.
  • Redirect URIs, scopes y consola del proveedor están documentados.
  • Logs excluyen code, verifier, token y cookie.
  • Los errores no muestran detalles internos.
  • Las pruebas cubren éxito, mismatch, expiración y reutilización.
  • El código generado por Claude Code se revisó contra fuentes oficiales.

La formación y consultoría de ClaudeCodeLab puede convertir esto en un taller: traer un flujo OAuth existente, mapear riesgos, añadir pruebas y salir con una checklist reutilizable. Consulta ClaudeCodeLab training.

Verificación práctica

La demo permite recorrer inicio de login, autorización simulada, callback, intercambio de token y dashboard. Si cambias el state, aparece state mismatch; si el verifier es incorrecto, el endpoint devuelve invalid_code_verifier. En prototipos OAuth, los fallos que más se escapan no son los felices, sino varias pestañas, botón atrás, códigos expirados y logs accidentales. Inclúyelos en el primer prompt de Claude Code.

Resumen

La calidad de OAuth depende de proteger toda la transacción. Usa Authorization Code + PKCE, valida state y nonce, compara redirect URI exactamente y guarda tokens del lado del servidor. Claude Code acelera, pero solo con restricciones claras y revisión contra documentación primaria.

#Claude Code #OAuth #authentication #security #TypeScript
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.