En el mundo actual de la ciberseguridad, el estándar OAuth ha evolucionado más allá de ser una simple solución de autenticación unificada. Tanto las identidades humanas como las no humanas dependen de él para satisfacer diversas necesidades críticas de autenticación. Sin embargo, a medida que OAuth se ha ampliado más allá de las aplicaciones del lado del servidor, ha surgido una pregunta crucial: ¿cómo pueden los clientes públicos demostrar su identidad sin un secreto almacenado?
Ahí es donde entra en juego el Proof Key for Code Exchange (PKCE), un mecanismo diseñado para fortalecer el flujo del código de autorización, el cual permite que los usuarios se autentiquen a través de una redirección en el navegador.
El Problema del ‘Check-in de Abrigos’
Imagina que estás en una conferencia y dejas tu mochila en un lugar de check-in. La norma es que recibes un ticket de reclamación, lo presentas y recuperas tu mochila. Pero, ¿qué pasa si alguien está mirando y ve el número del ticket? Esto abre la posibilidad de que se abuse de este “secreto compartido” y diga “ticket 47” para recoger tu mochila.
Ahora imagina un sistema más inteligente:
- Cuando dejas tu mochila, inventas una frase secreta: “elefante morado bailando”.
- Susurras una versión encriptada de esa frase al encargado (ellos no pueden revertir la encriptación).
- Escriben la versión encriptada junto a tu mochila.
- Más tarde, regresas y dices la frase original.
- Ellos la encriptan y comprueban si coincide con lo que escribieron.
Alguien que escuchó la versión encriptada no puede revertir fácilmente “elefante morado bailando”.
En resumen, eso es PKCE. La frase secreta es tu code_verifier y la versión encriptada es el code_challenge. El encargado representa al servidor de autorización, y el ticket de reclamación es el código de autorización. PKCE garantiza que, incluso si alguien intercepta tu código, no pueda usarlo.
¿Necesitas PKCE?
La respuesta corta es: sí, si estás utilizando el flujo de código de autorización.
| Tipo de Cliente | Clasificación | PKCE |
|---|---|---|
| Aplicación de una sola página (SPA) | Público | Requerido – no se pueden almacenar secretos en el navegador |
| Aplicación celular | Público | Requerido – los esquemas de URL pueden ser secuestrados, los binarios pueden ser descompilados |
| Aplicación de escritorio | Público | Requerido – los binarios pueden ser descompilados |
| Herramienta CLI | Público | Requerido – se ejecuta en la máquina del usuario |
| Aplicación del lado del servidor | Confidencial | Recomendado – defensa en profundidad contra inyecciones de código |
No aplica a:
- Credenciales de cliente – máquina a máquina, no hay código de autorización
- Concesiones de token de actualización – no se involucra ningún código de autorización
- Flujo implícito – obsoleto, los tokens se devuelven directamente sin un código
- Autorización de dispositivos – mecanismo diferente, no hay redirección para interceptar
Si tu flujo involucra un código de autorización, PKCE debe protegerlo. La OAuth 2.0 Security Best Current Practice ahora recomienda PKCE para todos los clientes de OAuth, no solo para los públicos. Aporta una defensa adicional que significa que, incluso si tienes un secreto de cliente, PKCE protege contra ataques de inyección de código de autorización.
El Problema que Resuelve PKCE
OAuth tradicional con un secreto de cliente fue diseñado pensando en aplicaciones del lado del servidor, ya que el secreto permanece en tu servidor y no está expuesto durante el tráfico de autenticación.
El desafío surge cuando consideramos aplicaciones SPA y celulares, una tendencia moderna emergente. No hay un lugar seguro para almacenar un secreto cuando el código se ejecuta en un dispositivo externo, y cualquiera que intercepte el código de autorización puede intercambiarlo por tokens.
PKCE resuelve esto al vincular la solicitud de token a la solicitud de autorización original sin requerir un secreto almacenado.
Mecanismo Central
Regresando al tema del check-in: en lugar de probar la identidad con un secreto que almacenas (el número del ticket 47), pruébalo demostrando que iniciaste la solicitud original (elefante morado bailando).
- Solicitud previa a la autenticación: Genera una cadena aleatoria (el
code_verifier) y mantenla en memoria - Solicitud de autenticación: Envía un hash de esa cadena (el
code_challenge) - Solicitud de token: Envía el
code_verifieroriginal - Verificación del servidor: Hashea el verificador, compara con el desafío almacenado
Un atacante que intercepte el código de autorización no tiene el verificador original y no puede completar el intercambio de tokens.
La Implementación Real
Paso 1: Generar el Code Verifier
El verificador es una cadena aleatoria criptográficamente segura, de 43-128 caracteres, que solo utiliza [A-Za-z0-9-._~]:
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
Paso 2: Derivar el Code Challenge
El desafío es un hash SHA-256 del verificador, codificado en base64url:
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}
Un dato interesante. Dado que SHA-256 siempre produce 32 bytes, el base64url de 32 bytes (256/6 = 42.67).
El desafío siempre tendrá 43 caracteres.
Paso 3: Solicitud de Autorización
Incluye el desafío (no el verificador) en tu solicitud de autorización:
GET /authorize?
response_type=code&
client_id=your-app&
redirect_uri=https://yourapp.com/callback&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256&
state=xyz123&
scope=openid profile
El servidor de autorización almacena el desafío junto a tu sesión de autorización.
Paso 4: Intercambio de Tokens
Después de que el usuario se autentica y recibes el código, intercámbialo por el verificador original:
const response = await fetch('/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: 'https://yourapp.com/callback',
client_id: 'your-app',
code_verifier: storedCodeVerifier // El original, no el hash
})
});
El servidor hashea el verificador que envías y lo compara con el desafío almacenado.
Coincidencia = tokens emitidos. Sin coincidencia = solicitud rechazada.
Errores Comunes de Configuración
Usar plain en lugar de S256: La especificación técnicamente permite enviar el verificador sin hashear como desafío (code_challenge_method=plain). No lo hagas. Existe solo para clientes que no pueden hacer SHA-256, lo cual es prácticamente nada moderno.
Siempre usa S256. Si no puedes, hay problemas más grandes.
Almacenamiento inseguro del verificador: El verificador debe vivir solo en memoria. No lo pongas en localStorage, sessionStorage o cookies.
Para SPAs, mantenlo en una variable de JavaScript. Para celulares, usa memoria segura.
Verificador demasiado corto: Debe tener entre 43 y 128 caracteres. Por debajo de 43, el servidor debería rechazarlo.
Codificación base64url incorrecta: El base64 estándar usa + y /. El base64url usa - y _. Mezclarlos rompe todo. También elimina el relleno =.
Viendo Todo en Acción
Leer sobre PKCE es una cosa, pero ver el intercambio de desafío/verificador en acción es fascinante.
Ejecuta todo el flujo de PKCE en el Looking Glass de ProtocolSoup
Protocol Soup fue construido específicamente para esto: llevar la tangibilidad y la interactividad a los protocolos de autenticación y la identidad en general. Ejecuta el flujo real contra un servidor de autorización real e inspecciona cada parámetro en cada paso.
El Looking Glass te muestra el code_challenge exacto enviado en la solicitud de autenticación, luego el code_verifier en la solicitud de token, para que puedas verificar la relación criptográfica tú mismo y comparar con un flujo de código de autorización tradicional.
Cuando PKCE No Es Suficiente
La identidad es una frontera en constante crecimiento y PKCE fue diseñado para proteger el intercambio de códigos de autorización.
No protege contra:
- Robos de tokens después de la emisión: Una vez que tienes tokens, el trabajo de PKCE ha terminado. Almacénalos y transfiérelos de manera segura.
- URI de redirección comprometida: Si un atacante controla la URI de redirección, obtiene el código y puede observar tu solicitud de token. PKCE no puede ayudar aquí.
- XSS en tu aplicación: Si los atacantes pueden ejecutar JavaScript en tu aplicación, pueden robar el verificador de la memoria antes de que lo uses.
En esencia, PKCE se puede resumir en un principio: prueba que iniciaste lo que intentas terminar.
Para la fuente autoritativa, consulta RFC 7636.
Para las mejores prácticas actuales de OAuth 2.0, consulta RFC 9700.

