Definition
Imagina un escenario aterrador: estás usando la app de tu banco en el móvil, y en algún lugar del fondo, una app maliciosa está observando tu tráfico de red. Te autenticas a través de OAuth, el servidor de autorización envía un código de vuelta, y… la app maliciosa lo intercepta. Ahora tienen todo lo que necesitan para robar tus tokens y acceder a tu cuenta bancaria. Este ataque era tan efectivo contra apps móviles que OAuth necesitaba un parche. Ese parche es PKCE - Proof Key for Code Exchange (pronunciado “pixy”).
PKCE añade un handshake criptográfico al flujo de código de autorización de OAuth. Antes de iniciar sesión, tu app genera un secreto aleatorio llamado “code verifier” - una cadena larga e impredecible. Luego crea un “code challenge” hasheando ese verifier con SHA-256. Cuando tu app solicita autorización, envía el code challenge. Cuando después intercambia el código de autorización por tokens, envía el verifier original. El servidor de autorización comprueba: ¿este verifier hashea al challenge que recibí antes? Si sí, procede. Si no, alguien interceptó el código e intenta usarlo.
La belleza de PKCE está en su simplicidad y la imposibilidad de hacer trampa. Un atacante que intercepta el código de autorización no tiene el code verifier - nunca se transmitió, solo el hash. Y no puedes revertir un hash SHA-256 para obtener el valor original. Así que incluso con el código de autorización en mano, el atacante no puede completar el intercambio de tokens. Es como enviar por correo la mitad de una llave de dos partes: inútil sin la otra mitad, que nunca salió de tu posesión. PKCE ahora es obligatorio en OAuth 2.1, reconociendo que este vector de ataque era demasiado peligroso para dejarlo como opcional.
Example
PKCE protege los flujos OAuth donde los códigos de autorización viajan por entornos no confiables:
Apps de Banca Móvil: Cuando abres tu app de BBVA o Santander y pulsas “Iniciar sesión con Google,” la app genera un code verifier PKCE y lo almacena de forma segura. Solo envía el hash (challenge) al endpoint de autorización de Google. Después de autenticarte, Google redirige de vuelta con un código de autorización. Incluso si una app maliciosa intercepta esta redirección (un vector de ataque real en Android), no puede intercambiar el código sin el verifier que nunca salió de la app legítima del banco.
Aplicaciones de Página Única (SPAs): El reproductor web de Spotify usa PKCE cuando te autentica. Como JavaScript corre en el navegador donde cualquiera puede ver el código fuente, no hay lugar para ocultar un client secret. PKCE resuelve esto: el code verifier vive solo en el session storage de tu navegador, enviado al servidor de Spotify solo durante el intercambio de tokens. El challenge enviado inicialmente no prueba nada sin el verifier para completar el handshake.
Aplicaciones de Escritorio: La app de escritorio de Slack usa PKCE cuando inicias sesión. Genera el verifier, abre tu navegador para la autenticación con Google/Okta, y espera el callback. El sistema operativo enruta el callback de vuelta a Slack, que entonces intercambia el código usando su verifier secreto. Incluso si otra aplicación registró la misma URL de callback (un ataque documentado), no tendría el verifier.
Herramientas CLI: El CLI de GitHub usa PKCE durante gh auth login. Genera un verifier, abre tu navegador en github.com/login/device, te autenticas allí, y cuando vuelves a la terminal, el CLI usa su verifier para completar el intercambio. Sin secretos almacenados en archivos de configuración en texto plano.
Apps de Smart TV: Cuando Netflix en tu TV te pide autenticarte, usa PKCE con el flujo de dispositivo. La TV muestra un código, visitas netflix.com/activate en tu móvil, y la TV hace polling esperando la confirmación. PKCE asegura que incluso si alguien ve el código mostrado, no pueden secuestrar tu autenticación porque no tienen el verifier que la TV generó.
Analogía
El Sistema del Sobre Sellado: Imagina que estás enviando un paquete valioso a través de un mensajero poco fiable. Antes de enviarlo, escribes una frase secreta en un papel y la sellas en un sobre que guardas contigo. Le dices al destinatario: “Estoy enviando un paquete. Cuando llegue, te llamaré con la frase secreta. Solo entrégalo a quien sepa la frase.” Incluso si el mensajero roba el paquete, no puede reclamarlo - no conoce la frase que nunca salió de tu posesión.
La Caja de Seguridad de Doble Llave: Los bancos a veces usan sistemas de dos llaves donde tanto el cliente como el banco deben insertar sus llaves simultáneamente. PKCE es similar: el código de autorización es como la llave del banco (viajó por la red), pero solo funciona cuando se combina con el code verifier (tu llave que nunca salió de tu dispositivo). Tener una sola llave no abre nada.
El Ticket con Talón: En el servicio de valet de un restaurante de lujo, rompen tu ticket por la mitad - tú guardas una pieza con un patrón único, ellos guardan la otra. Cuando vuelves, ambas mitades deben coincidir exactamente en el patrón del desgarro. Un ladrón que fotografió la mitad del valet todavía no puede reclamar tu coche porque no tiene tu mitad con su patrón de desgarro único que nunca fue expuesto.
La Cerradura de Combinación en Dos Partes: Imagina una cerradura donde estableces la primera mitad de la combinación, luego susurras solo un hash de ella al cerrajero. Después, vuelves y marcas la combinación completa. El cerrajero comprueba: ¿el hash de lo que acabas de introducir coincide con lo que susurraste antes? Si alguien escuchó solo el hash, todavía no pueden abrir la cerradura - necesitarían revertir tus números originales desde el hash, lo cual es matemáticamente imposible.
Diagrama
sequenceDiagram
participant App as Aplicación Cliente
participant Auth as Servidor de Autorización
Note over App: Generar par PKCE
App->>App: 1. Crear code_verifier
(aleatorio 43-128 chars)
App->>App: 2. Crear code_challenge
= SHA256(code_verifier)
App->>App: 3. Almacenar code_verifier de forma segura
App->>Auth: 4. Petición de autorización
(code_challenge + method=S256)
Note over Auth: Almacenar code_challenge
con sesión de auth
Auth->>App: 5. Código de autorización
Note over App: Incluso si el código es interceptado...
App->>Auth: 6. Petición de token
(código + code_verifier)
Auth->>Auth: 7. Verificar: SHA256(code_verifier)
== code_challenge almacenado?
alt Verificación exitosa
Auth->>App: 8. Access token + Refresh token
else Verificación falla (atacante)
Auth->>App: 8. Error: invalid_grant
Note over Auth: Atacante no puede adivinar
code_verifier desde el hash
end
Code Example
// PKCE implementation
const crypto = require('crypto');
// Generate code verifier (random string)
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
// Generate code challenge (SHA256 hash of verifier)
function generateCodeChallenge(verifier) {
return crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
}
// Step 1: Start authorization flow with PKCE
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
// Store verifier securely (session storage for SPAs)
sessionStorage.setItem('pkce_verifier', codeVerifier);
// Redirect to authorization server with challenge
const authUrl = new URL('https://oauth.provider.com/authorize');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('client_id', 'your_client_id');
authUrl.searchParams.append('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
authUrl.searchParams.append('scope', 'read:user');
authUrl.searchParams.append('state', generateRandomState());
window.location.href = authUrl.toString();
// Step 2: Exchange authorization code with verifier
const tokenResponse = await fetch('https://oauth.provider.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
client_id: 'your_client_id',
redirect_uri: 'https://yourapp.com/callback',
code_verifier: sessionStorage.getItem('pkce_verifier')
})
});
// Server verifies: SHA256(code_verifier) === code_challenge
Notas de Seguridad
CRÍTICO - …
Configuración y Validación:
- PKCE es OBLIGATORIO para clientes públicos (SPAs, apps móviles) y RECOMENDADO para clientes confidenciales en OAuth 2.1.
- El code verifier debe ser criptográficamente aleatorio (mínimo 43 caracteres, máximo 128).
- Usar método S256 (SHA-256), nunca el método plain.
- Almacenar el code verifier de forma segura en el cliente (session storage, secure storage para móvil).
- El code verifier no debe reutilizarse entre solicitudes de autorización.
Monitoreo y Protección:
- Limpiar el code verifier después del intercambio de tokens.
- PKCE protege contra la interceptación del código de autorización pero el cliente aún debe validar el parámetro state para protección CSRF.
- PKCE no reemplaza el client secret para clientes confidenciales.
- El servidor de autorización debe validar la coincidencia challenge/verifier.
- Implementar tanto en el lado del cliente como del servidor.