Entrada

PKCE: por qué el flujo de autorización cambió para siempre con RFC 7636

Origen, mecanismo y consecuencias de Proof Key for Code Exchange, la extensión que convirtió Authorization Code en el flujo seguro por defecto para clientes públicos y, finalmente, para todos.

PKCE: por qué el flujo de autorización cambió para siempre con RFC 7636

Proof Key for Code Exchange empezó como una mitigación quirúrgica para un problema específico de las aplicaciones móviles y ha terminado convirtiéndose en la configuración estándar del flujo de autorización moderno, obligatoria incluso para clientes confidenciales en OAuth 2.1. Este artículo forma parte de la serie La evolución de la seguridad en aplicaciones modernas y recorre el camino que llevó de “parche para móviles” a “defensa en profundidad para todos”.

El ataque que motivó el estándar

Publicado como RFC 7636 en septiembre de 2015, PKCE nace para cerrar una vulnerabilidad concreta del flujo Authorization Code en clientes públicos: aplicaciones que no pueden custodiar un secreto. Conviene recordar la distinción.

Un cliente confidencial corre en un entorno controlado (servidor propio) y puede guardar credenciales de cliente de forma segura. Cuando canjea un code por tokens en el token endpoint, se autentica con client_id + client_secret (o mTLS, o assertion JWT). Nadie más puede completar ese canje.

Un cliente público no tiene esa posibilidad. Una aplicación móvil distribuida a millones de dispositivos, una SPA cuyo código vive entero en el navegador, una CLI instalada localmente: todos ellos no pueden incluir un secreto sin asumir que cualquier atacante podrá extraerlo. OAuth 2.0 admitía estos clientes sin autenticación en el token endpoint, confiando únicamente en el conocimiento del code para validar el canje.

El problema llegó por los esquemas de redirección en móvil. iOS y Android permitían a una aplicación registrar un custom URL scheme (por ejemplo, miapp://callback). Varias aplicaciones instaladas simultáneamente podían registrar el mismo scheme, y el sistema operativo no garantizaba cuál recibiría el redirect. Un atacante que instalara una aplicación maliciosa con el mismo scheme que una legítima podía interceptar el authorization code destinado a la víctima y canjearlo por tokens, sin secreto que se lo impidiera. El ataque era viable, documentado y explotable en la práctica.

La mecánica de PKCE

La idea es elegante: en lugar de depender de un secreto estático, el cliente demuestra en el canje que es el mismo que inició el flujo, utilizando un secreto efímero y único por transacción que nunca viaja por el canal vulnerable.

El cliente, antes de redirigir al usuario, genera un code_verifier: una cadena aleatoria de entre 43 y 128 caracteres del conjunto [A-Z] [a-z] [0-9] -._~, criptográficamente aleatoria. De ese verifier deriva un code_challenge aplicando SHA-256 y codificando el resultado en base64url sin padding. El cliente envía el code_challenge y el code_challenge_method (S256) en la petición de autorización al authorization endpoint. El Authorization Server almacena ambos asociados al code emitido.

Cuando el cliente recibe el code y llega al token endpoint, envía el code_verifier original. El servidor aplica SHA-256, compara con el challenge almacenado y solo acepta el canje si coinciden. Un atacante que haya interceptado el code no dispone del verifier —este nunca salió del cliente legítimo— y no puede completar el canje.

S256 frente a plain

El RFC admite dos métodos. plain envía el verifier directamente como challenge; protege únicamente contra escenarios donde el atacante puede ver la petición al token endpoint pero no la petición al authorization endpoint, un threat model raro. S256 aplica el hash y es la única opción razonable: su uso es obligatorio para cualquier cliente que pueda calcular SHA-256, lo que incluye a todos en la práctica. OAuth 2.1 elimina plain directamente.

De mitigación a norma general

El recorrido posterior de PKCE es interesante. El RFC lo describe como defensa para clientes públicos; los Security Best Current Practices para OAuth 2.0 fueron recomendando su uso en más escenarios, y OAuth 2.1 (el draft que unifica las buenas prácticas en un documento único) lo declara obligatorio para todos los clientes, también los confidenciales.

¿Por qué extenderlo a clientes con secreto? Defensa en profundidad. Un client_secret filtrado —un commit descuidado en GitHub, un log mal configurado, un dump de base de datos— permite a un atacante completar canjes si además intercepta un code. PKCE añade una capa independiente: incluso con el secreto comprometido, el atacante necesitaría el verifier, que solo vive en memoria durante la transacción concreta. La composición de las dos defensas eleva sustancialmente el coste del ataque.

SPAs: del Implicit a Authorization Code + PKCE

El cambio más visible para los arquitectos web ha sido en las aplicaciones de página única. Durante años, la recomendación fue el flujo Implicit, porque una SPA no podía canjear un code sin secreto de forma segura. PKCE elimina esa limitación: la SPA genera su verifier, envía el challenge, recibe el code y lo canjea sin secreto, demostrando la transacción con el verifier.

Con esa pieza en su sitio, Implicit pierde toda razón de ser. Sus problemas —token en el fragmento de URL, sin refresh token estándar, expuesto a cualquier script en la página— desaparecen al pasar a Authorization Code con PKCE. Las BCP actuales y los principales IdPs (Keycloak, Okta, Auth0) lo dan por sentado.

La siguiente pregunta operativa es dónde almacenar los tokens en la SPA. localStorage es accesible desde cualquier XSS. Cookies httpOnly requieren que el Resource Server esté en el mismo dominio o maneje CORS con credenciales. La solución más robusta hoy es el patrón BFF (Backend for Frontend, ver API Gateway, BFF y Service Mesh): un backend propio actúa como cliente confidencial, mantiene los tokens en sesión server-side y presenta cookies httpOnly al navegador. La SPA no maneja tokens; el BFF hace el flujo OAuth por ella.

Aplicaciones móviles: PKCE y más allá

En móvil, PKCE resuelve la intercepción de code pero no elimina otros vectores. Los custom URL schemes siguen siendo débiles porque cualquier aplicación puede registrarlos. La solución moderna son los Universal Links (iOS) y App Links (Android): asociaciones verificadas mediante un archivo servido bajo HTTPS en el dominio del Authorization Server que garantizan que solo la aplicación legítima recibe el redirect.

La combinación correcta para una aplicación móvil actual es, por tanto: Authorization Code + PKCE + Universal/App Links + uso del in-app browser tab del sistema (SFSafariViewController o Chrome Custom Tabs) en lugar de WebViews embebidos, que filtran credenciales al contenedor de la aplicación. RFC 8252 (octubre de 2017), OAuth 2.0 for Native Apps, documenta este conjunto de recomendaciones y es lectura obligada antes de implementar un flujo móvil.

Errores de integración habituales

Aunque el mecanismo es conceptualmente simple, las integraciones fallan en detalles predecibles.

El verifier corto o predecible rompe la defensa. 43 caracteres es el mínimo por RFC; usar menos, o generarlo con Math.random() en lugar de un PRNG criptográfico, degrada la entropía y abre ataques de fuerza bruta sobre el challenge. Librerías estándar (crypto.getRandomValues, SecureRandom) resuelven el tema sin esfuerzo.

El reuso del verifier entre transacciones es un error sutil. Cada flujo de autorización debe generar un verifier nuevo. Reutilizar el mismo implica que un atacante que lo obtenga una vez (logs, memoria comprometida) podrá completar canjes futuros mientras el verifier siga vivo en el cliente.

La aceptación de plain en el Authorization Server es una mala configuración que se ve con frecuencia en despliegues antiguos. Cualquier IdP actual debería rechazar plain por defecto y aceptar únicamente S256.

La omisión del challenge por parte del cliente cuando el IdP no lo exige es otra señal de alarma. Si el Authorization Server no fuerza PKCE en clientes públicos, el cliente debería igualmente enviarlo, y el equipo de plataforma debería configurar el IdP para exigirlo. Sin PKCE obligatorio en el servidor, un atacante puede iniciar un flujo sin challenge y seguir siendo vulnerable al ataque original.

Un check rápido sobre cualquier integración existente: inspeccionar la petición al authorization endpoint. Si no aparecen code_challenge y code_challenge_method=S256, la integración está por debajo de la línea base actual, independientemente del tipo de cliente.

Relación con DPoP y el futuro

PKCE protege el canje del code. No protege el access token una vez emitido: si alguien roba el token, lo usará. Aquí entra DPoP (Demonstrating Proof of Possession, RFC 9449, septiembre de 2023), que vincula cada petición al Resource Server con una prueba de posesión de una clave privada del cliente. La combinación PKCE + DPoP cierra el ciclo: transacción segura en la emisión y uso vinculado en cada petición. Es la dirección hacia la que apuntan las evoluciones de OAuth, incluyendo FAPI 2.0 para ecosistemas financieros.

Cuándo desactivarlo

La respuesta corta: nunca. PKCE apenas tiene coste operativo —un hash SHA-256 y un campo extra en dos peticiones— y aporta defensa real. Los argumentos que aparecen ocasionalmente (“mi cliente es confidencial, no hace falta”, “tenemos mTLS en el token endpoint, es redundante”) son micro-optimizaciones que sacrifican defensa en profundidad a cambio de ahorrarse complejidad inexistente. La decisión por defecto sana es activarlo y exigirlo en el IdP.

Artículos relacionados en esta serie

Referencias

  • RFC 7636 (septiembre 2015). Proof Key for Code Exchange by OAuth Public Clients.
  • RFC 8252 (octubre 2017). OAuth 2.0 for Native Apps.
  • RFC 9449 (septiembre 2023). OAuth 2.0 Demonstrating Proof of Possession (DPoP).
  • RFC 9700 (2025). Best Current Practice for OAuth 2.0 Security.
  • IETF OAuth Working Group. OAuth 2.1 Draft.
  • OpenID Foundation (2024). FAPI 2.0 Security Profile.
Esta entrada está licenciada bajo CC BY 4.0 por el autor.