OAuth 2.0 y PKCE: Por qué tu flujo de autenticación es vulnerable (y cómo arreglarlo)

Si estás implementando OAuth 2.0 en una aplicación móvil o SPA (Single Page Application) y confías únicamente en el Authorization Code Flow estándar, tienes una puerta trasera abierta. Un atacante puede interceptar tu código de autorización y secuestrar la sesión del usuario en milisegundos. Aquí te explicamos cómo cerrarla con PKCE.

PKCE (Proof Key for Code Exchange) es una extensión de seguridad para OAuth 2.0 que previene la intercepción del código de autorización. Funciona creando una clave secreta única ("verifier") para cada solicitud de inicio de sesión, que vincula criptográficamente la solicitud inicial con el intercambio final del token.

El Problema: La Analogía del "Ticket de Valet Parking"

Analogía: Imagina que dejas tu coche en un valet parking. El encargado te da un ticket de papel (el Authorization Code). La regla es simple: "Quien tenga el ticket, se lleva el coche (el Access Token)".

Si se te cae el ticket y alguien más lo recoge, puede ir a la ventanilla y llevarse tu coche. El encargado no verifica quién eres, solo verifica que el ticket sea válido. Esto es OAuth 2.0 sin PKCE.

En el mundo digital, este "robo de ticket" ocurre mediante la Intercepción de Custom URL Schemes. En dispositivos móviles, múltiples aplicaciones pueden registrarse para abrir el mismo esquema de URL (ej. myapp://callback). Una app maliciosa instalada en el mismo dispositivo puede "escuchar" este callback, robar el código y canjearlo por un token antes que tú.

Con PKCE, cuando entregas el coche, también susurras una contraseña secreta hash (el Code Challenge) y te guardas la original. Cuando vuelves con el ticket, el encargado te exige la contraseña original (el Code Verifier). Si el ladrón trae el ticket pero no sabe la contraseña, no se lleva el coche.

Implementación Técnica (JavaScript Moderno)

Para implementar PKCE, necesitamos generar dos valores en el cliente antes de iniciar el flujo de autenticación:

  1. code_verifier: Una cadena aleatoria criptográficamente fuerte.
  2. code_challenge: Un hash SHA-256 del verifier, codificado en Base64URL.

A continuación, una implementación sin dependencias externas usando la Web Crypto API estándar:


// Utilidad para codificar en Base64 URL-safe (RFC 4648)
const base64UrlEncode = (arrayBuffer) => {
  return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
};

// 1. Generar el Code Verifier (Aleatorio)
const generateCodeVerifier = () => {
  const array = new Uint8Array(32); // 32 bytes de entropía
  window.crypto.getRandomValues(array);
  return base64UrlEncode(array);
};

// 2. Generar el Code Challenge (SHA-256)
const generateCodeChallenge = async (verifier) => {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await window.crypto.subtle.digest('SHA-256', data);
  return base64UrlEncode(hash);
};

// Flujo de Ejecución
const iniciarLogin = async () => {
  const verifier = generateCodeVerifier();
  const challenge = await generateCodeChallenge(verifier);

  // IMPORTANTE: Guardar el verifier en SessionStorage para usarlo después
  sessionStorage.setItem('pkce_verifier', verifier);

  // Construir URL de autorización
  const authUrl = `https://tu-auth-server.com/authorize?` +
    `response_type=code` +
    `&client_id=TU_CLIENT_ID` +
    `&redirect_uri=${encodeURIComponent('https://tu-app.com/callback')}` +
    `&code_challenge=${challenge}` +
    `&code_challenge_method=S256`; // SIEMPRE usar S256

  window.location.href = authUrl;
};

Paso final (El canje): Cuando el usuario regresa a tu app con el code, debes enviar una petición POST al endpoint de token incluyendo el code_verifier original que guardaste en el almacenamiento local. El servidor hará el hash del verifier y lo comparará con el challenge que recibió al inicio. Si coinciden, el token es emitido.

Advertencia de Seguridad: Nunca uses code_challenge_method=plain a menos que sea técnicamente imposible usar SHA-256. El método "plain" no protege contra ciertos vectores de ataque y está desaconsejado en OAuth 2.1.

Preguntas Frecuentes (FAQ)

P. ¿Necesito PKCE si tengo un backend (Cliente Confidencial)?

R. Sí. Aunque los clientes confidenciales (que pueden guardar un client_secret) son menos vulnerables a la intercepción directa, las mejores prácticas actuales (incluyendo el borrador de OAuth 2.1) recomiendan PKCE para todos los clientes. Añade una capa de defensa contra ataques de inyección de código (Code Injection).

P. ¿Cuál es la diferencia entre el flujo Implícito y PKCE?

R. El flujo Implícito (Implicit Flow) devolvía el token directamente en la URL, lo cual es altamente inseguro y está obsoleto. El flujo con PKCE utiliza el canal de intercambio de código, nunca expone el token en la URL y verifica la identidad del cliente, siendo el estándar moderno para SPAs.

P. ¿Qué pasa si el servidor de autorización no soporta S256?

R. Si el servidor no soporta S256, es una señal de alarma. Deberías considerar migrar a un proveedor de identidad moderno (como Auth0, Okta, o Keycloak actualizado) que cumpla con los estándares actuales de seguridad IETF.

Post a Comment