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).

  1. Solicitud previa a la autenticación: Genera una cadena aleatoria (el code_verifier) y mantenla en memoria
  2. Solicitud de autenticación: Envía un hash de esa cadena (el code_challenge)
  3. Solicitud de token: Envía el code_verifier original
  4. 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.

Por Editor

Deja un comentario