Autenticación sin estado con JWT: Guía práctica de RFC 9700 y mejores prácticas de seguridad OAuth

Guía práctica sobre autenticación sin estado con JWT basada en la RFC 9700 y las mejores prácticas de seguridad OAuth para proteger APIs y aplicaciones.

    Artículo elaborado por:

    Javier Gil Pascual

    Analista Programador de Software

    Máster en Ingeniería Informática. Especialista en el desarrollo de aplicaciones web con herramientas actuales para JavaScript y TypeScript, con experiencia en arquitecturas full stack.

    Apasionado por el aprendizaje continuo y por crear soluciones de software bien diseñadas, mantenibles y que realmente mejoren la vida de las personas.

    José Collado

    Desarrollador Full Stack

    Desarrollador Full Stack especializado en Angular y Spring Boot, con interés en User Experience y Usabilidad.

Contenidos

  1. ¿Qué significa “autenticación sin estado” con JWT?
  2. RFC 9700: El compendio definitivo de mejores prácticas
  3. Flujos deprecados: qué evitar y por qué
  4. Vulnerabilidades críticas y cómo mitigarlas
  5. Restricción de privilegios: aud, scopes y el principio de mínimo privilegio
  6. Autenticación de clientes: Más allá del Client Secret
  7. Otras recomendaciones clave de RFC 9700
  8. Configuración práctica en Spring Security
  9. OAuth 2.0 vs OAuth 2.1: La evolución hacia la seguridad por defecto
  10. Recursos adicionales
  11. Conclusión

OAuth 2.0 y OpenID Connect se han convertido en el estándar de facto para proteger APIs y gestionar autenticación delegada en aplicaciones modernas. Sin embargo, el aumento exponencial de ataques contra implementaciones mal configuradas ha demostrado que seguir “las prácticas de siempre” ya no es suficiente. La RFC 9700, publicada en 2025, recopila las mejores prácticas actualizadas y depreca flujos inseguros que han sido fuente de vulnerabilidades durante años.

En este artículo exploraremos las recomendaciones clave de RFC 9700 para implementar autenticación sin estado con JSON Web Tokens (JWT) de forma segura, basándonos en casos reales y ejemplos prácticos.

¿Qué significa “autenticación sin estado” con JWT?

En un modelo stateless, el servidor no mantiene sesiones: el cliente presenta un JWT en cada petición y el servidor de recursos (API) decide si concede acceso validando la firma del token y sus claims (caducidad, emisor, audiencia, etc.).

Este enfoque escala horizontalmente de forma excelente porque no requiere un almacén de sesiones compartido, pero desplaza toda la responsabilidad de seguridad a tres momentos críticos: cómo se emite el token, cómo se transporta y cómo se valida. Cualquier debilidad en estos puntos se convierte en acceso directo para un atacante.

Antes de profundizar en las vulnerabilidades, es importante entender los cuatro roles que participan en un flujo OAuth/OpenID Connect:

  • Resource Owner: El usuario propietario de los datos (usuario)
  • Client Application (o Relying Party en OpenID Connect): La aplicación web, móvil o SPA que necesita consumir datos del usuario
  • Authorization Server (u OpenID Provider): El servidor que autentica y autoriza al usuario, emitiendo tokens
  • Resource Server: El API o servidor que almacena los datos del usuario y valida los tokens de acceso

Cada buena práctica y mitigación de RFC 9700 está enfocada en uno o varios de estos roles.

RFC 9700: El compendio definitivo de mejores prácticas

La RFC 9700 no reemplaza las especificaciones anteriores, sino que las complementa con un enfoque práctico:

  • Define un modelo actualizado de atacante que incluye web attackers, network attackers y atacantes capaces de registrar clientes válidos
  • Analiza amenazas modernas con contramedidas específicas
  • Depreca oficialmente flujos históricamente problemáticos: Password Grant e Implicit Grant
  • Recomienda el uso obligatorio de PKCE (Proof Key for Code Exchange) incluso en clientes confidenciales

Flujos deprecados: qué evitar y por qué

3.1. Implicit Grant: Por qué es hora de dejarlo atrás

El flujo implícito emite tokens directamente en la URL de redirección (en el fragmento), lo que genera múltiples vectores de ataque:

  • Los tokens quedan expuestos en el historial del navegador
  • Se filtran a través del header Referer en peticiones subsecuentes
  • Pueden ser capturados por scripts maliciosos

Recomendación: Migrar siempre a Authorization Code + PKCE, que mantiene los tokens fuera de la URL y añade protección criptográfica contra interceptación.

Excepción: Solo en entornos de testing con medidas muy estrictas, y nunca en producción.

3.2. Password Credentials Grant: El flujo que nunca debió usarse

Este flujo envía las credenciales del usuario en plano directamente al cliente, lo que genera problemas graves:

  • Mayor superficie de ataque: Las credenciales están dispersas por múltiples aplicaciones
  • Incompatible con MFA: Al no pasar por el Authorization Server, no se pueden aplicar políticas de autenticación de múltiples factores
  • Las credenciales salen del servidor de identidad: Violando el principio de centralización de la autenticación

Alternativa: Usar Client Credentials Grant para autenticación máquina-a-máquina, donde el token representa al cliente/proceso, no a un usuario.

Vulnerabilidades críticas y cómo mitigarlas

4.1. Validación insuficiente de redirect URIs

Este es uno de los vectores de ataque más comunes y peligrosos. Si un Authorization Server no valida estrictamente las URIs de redirección, un atacante puede redirigir el flujo a un sitio bajo su control.

Caso real: Booking.com

En el blog de Salt podemos encontrar un caso documentado donde Booking.com validaba únicamente el host (booking.com) pero no el path completo de la redirect URI. Esto permitió a investigadores de seguridad crear URLs maliciosas que redirigían a rutas no previstas, exponiendo códigos de autorización:

https://www.facebook.com/v3.0/dialog/oauth?redirect_uri=
https://account.booking.com/any/path/attacker/wants&response_type=code

Mitigación:

  • Validar protocolo (https://), host y path completo de las redirect URIs
  • Nunca usar comodines (*) en producción
  • Prohibir open redirectors en clientes y servidores
  • En desarrollo/testing, permitir localhost con puertos dinámicos solo para apps nativas

4.2. Fuga de credenciales en header Referer y URLs

Los códigos de autorización y tokens expuestos en query parameters pueden filtrarse de múltiples formas:

  • A través del header Referer al cargar recursos externos
  • En logs de servidor y proxies
  • En el historial del navegador

Mitigación:

  • Configurar Referrer-Policy: no-referrer o strict-origin-when-cross-origin
  • Evitar pasar tokens en query parameters
  • Usar form_post response mode en lugar de redirecciones GET cuando sea posible
  • Cada aplicación debe tener su propio client_id y nunca reutilizar tokens entre aplicaciones

4.3. Ataques CSRF e inyección de código

Existen dos ataques relacionados que RFC 9700 aborda específicamente:

Cross-Site Request Forgery (CSRF): El atacante genera su propio flujo de autenticación y engaña a la víctima para que se loguee en la sesión del atacante. La víctima cree estar en su propia cuenta pero sus acciones benefician al atacante (por ejemplo, subir fotos privadas a la cuenta del atacante).

Inyección de código: El atacante roba el código de autorización de la víctima e intenta usarlo en su propio cliente malicioso para obtener tokens de la víctima.

Contramedidas:

  1. PKCE (Proof Key for Code Exchange): El cliente genera un code_verifier aleatorio, envía su hash (code_challenge) en la petición de autorización, y al intercambiar el código por tokens debe presentar el verifier original. Esto impide que un atacante use un código robado sin el verifier.
  2. Nonce (OpenID Connect): Un valor aleatorio que se envía en la petición de autorización y luego se incluye en el ID Token. El cliente valida que el nonce del token coincide con el generado originalmente
  3. State: Un token CSRF que va y vuelve en el flujo, permitiendo al cliente validar que la respuesta corresponde a su petición original

Importante: Estos parámetros deben ser únicos por sesión y generados dinámicamente, nunca valores estáticos.

Restricción de privilegios: aud, scopes y el principio de mínimo privilegio

Una práctica fundamental pero frecuentemente ignorada es limitar el alcance de los tokens

5.1. Audience (aud claim)

El claim aud especifica para qué Resource Server(s) se emitió el token. Si el API no valida audience, el claim no aporta seguridad.

Beneficio: Si se filtra un token emitido para el API de facturación, no podrá usarse contra el API de recursos humanos.

5.2. Scopes

Definen permisos específicos dentro de un recurso. Cuanto más granular sea el scope, menor será el impacto en caso de compromiso del token.

Ejemplo práctico: En una aplicación Angular, al integrar una librería de validación de JWT (express-oauth2-jwt-bearer), la librería exigía validar el claim audience antes de aceptar el token. Esto obligó a configurar correctamente el Authorization Server para incluir audience en los tokens y al Resource Server a validarlo.

Autenticación de clientes: Más allá del Client Secret

Cuando un cliente confidencial (backend) se autentica ante el Authorization Server, el método tradicional es enviar el client_secret (como HTTP Basic Auth). Pero esto tiene problemas:

  • El secreto se transmite en cada petición (aunque sea por HTTPS)
  • Es un secreto simétrico que debe almacenarse en ambos extremos

Alternativa recomendada: Private Key JWT

El cliente firma un JWT con su clave privada y lo envía al Authorization Server, que verifica la firma con la clave pública previamente registrada.

Ventajas:

  • No se transmiten secretos
  • Protección reforzada contra suplantación de clientes
  • El Authorization Server (por ejemplo, Keycloak) ya soporta este método configurando “JWT firmado con claves asimétricas” en lugar de “Client Secret POST”

Otras recomendaciones clave de RFC 9700

7.1. Authorization Server Metadata (Discovery)

Publicar un endpoint de discovery estándar (.well-known/openid-configuration) permite a los clientes y Resource Servers descubrir automáticamente:

  • Qué grant types están soportados
  • URLs de autorización, token y JWKs
  • Algoritmos de firma soportados

Esto reduce errores de configuración y facilita validaciones coherentes.

7.2. TLS extremo a extremo

Toda comunicación que involucre tokens debe usar HTTPS obligatoriamente:

  • Nunca permitir http:// en redirect URIs

Proteger contra ataques Man-in-the-Middle

7.3. postMessage y comunicación entre frames

En (renovación de tokens en iframes ocultos), es crítico validar el origin al usar postMessage para enviar tokens del iframe al frame principal. Un postMessage sin validación de origen enviaría tokens a todos los tabs del navegador.

7.4. CORS estricto

El Authorization Server debe configurar CORS de forma restrictiva:

  • Permitir CORS en endpoints de tokens (/token, /userinfo)
  • No permitir CORS en /authorize, que debe ejecutarse con redirección completa para mantener la sesión del navegador

Configuración práctica en Spring Security

Para Resource Servers en Spring Boot, la configuración recomendada es:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # Opción recomendada: usa discovery
          issuer-uri: https://accounts.miempresa.com
          
          # Validación de audience
          audiences:
            - https://api.miempresa.com

Ventajas de issuer-uri sobre jwk-set-uri:

  • Sigue el estándar OpenID Connect Discovery
  • Valida automáticamente el claim issuer
  • Si el proveedor cambia la URL del JWK Set, la aplicación se adapta automáticamente
  • Permite validación de audience con configuración adicional

La librería spring-security-oauth2-resource-server gestiona toda la validación: firma del JWT con el JWK Set, caducidad, issuer y audience

Configuración práctica en Angular

La librería angular-auth-oidc-client es la solución más adoptada en el ecosistema Angular para implementar OIDC. Soporta nativamente el Authorization Code Flow con PKCE y el Silent Renew mediante iframe oculto, alineándose con las recomendaciones de la RFC 9700 y las OAuth 2.0 Security Best Current Practices.

La configuración se centraliza mediante provideAuth:

El fichero silent-renew.html, que se sirve como asset estático, es el iframe oculto que ejecuta el flujo de renovación.

A diferencia de una implementación basada en postMessage, donde es imprescindible validar explícitamente el origin del mensaje para evitar comunicaciones cross-domain no deseadas, la librería utiliza un CustomEvent despachado directamente al window.parent.

Este mecanismo no emplea el canal de mensajería cross-origin del navegador, sino que funciona dentro del mismo contexto same-origin, ya que el silent-renew.html se sirve desde la propia aplicación. Por tanto, no es necesario realizar una validación manual del origin del parent, como sí sería obligatorio en un esquema con postMessage.

La seguridad en este caso no depende del origin, sino de las validaciones propias del protocolo OIDC (como state, nonce y la correlación del iframe), que la librería gestiona internamente.

OAuth 2.0 vs OAuth 2.1: La evolución hacia la seguridad por defecto

La siguiente imagen resume la evolución de OAuth:

OAuth 2.0 (hace 10 años):

  • 4 grant types (Authorization Code, Implicit, Password, Client Credentials)
  • Múltiples formas de pasar tokens (header, body, query string)

OAuth 2.1 (actualidad):

  • Deprecados: Implicit Grant, Password Grant, tokens en query parameters
  • Obligatorio: PKCE para todos los flujos
  • Añadidos: RFCs complementarios (PKCE, security best practices, browser-based apps, móviles)

RFC 9700 consolida toda esta evolución en un único documento “limpio” que elimina lo inseguro y refuerza las mejores prácticas

Recursos adicionales

Para profundizar sin tener que leer todas las RFCs, OWASP mantiene una guía específica de mejores prácticas OAuth y OIDC clasificada por roles (Authorization Server, Resource Server, Cliente)

También recomendamos revisar:

  • RFC 9700: OAuth 2.0 Security Best Current Practice
  • RFC 7636: PKCE
  • RFC 8252: OAuth for Native Apps
  • RFC 8693: Token Exchange

Conclusión

La autenticación sin estado con JWT ofrece escalabilidad y flexibilidad, pero requiere una implementación rigurosa para ser segura. RFC 9700 nos proporciona un marco actualizado que refleja años de ataques reales y lecciones aprendidas

Las claves para una implementación segura son:

  1. Usar Authorization Code + PKCE siempre (olvidar Implicit y Password Grant)
  2. Validar redirect URIs de forma estricta (protocolo + host + path)
  3. Nunca exponer tokens en URLs (ni query params ni fragmentos)
  4. Validar audience en Resource Servers
  5. Implementar rotación de refresh tokens en clientes públicos
  6. Considerar Private Key JWT para autenticación de clientes confidenciales
  7. Usar discovery (.well-known/openid-configuration) para automatizar configuraciones

La seguridad no es opcional ni algo que “se añade después”. En ecosistemas OAuth/OIDC, cada eslabón cuenta: desde cómo se configura el Authorization Server hasta cómo el último microservicio valida un JWT. RFC 9700 nos da las herramientas; aplicarlas correctamente es nuestra responsabilidad.