OAuth2 Resource Server con JWT

Intermedio
Spring Boot
Spring Boot
Actualizado: 04/05/2026

Diagrama: tutorial-spring-security-oauth2-resource-server-jwt

Qué es un Resource Server en OAuth2

En el modelo OAuth2 existen tres actores principales: el cliente que solicita acceso, el authorization server que autentica al usuario y emite los tokens, y el resource server que expone los recursos protegidos y confía en los tokens emitidos. En arquitecturas modernas los tokens son JWT firmados, lo que permite al resource server validarlos en local sin llamar al emisor en cada petición.

Spring Security proporciona el starter spring-boot-starter-oauth2-resource-server para implementar este rol sin escribir lógica criptográfica. El starter configura un filtro que intercepta la cabecera Authorization: Bearer, extrae el JWT, valida firma y expiración, y construye el contexto de seguridad con las authorities derivadas de los claims.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

El flujo típico es: el cliente obtiene un token del authorization server, lo envía al resource server en cada petición y este lo valida sin volver a consultar al emisor salvo que expire su caché de claves.

Configuración con issuer-uri

La forma más sencilla de configurar un resource server es apuntar al issuer URI del authorization server. Spring Security descubre automáticamente los metadatos y las claves públicas de firma.

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://sso.example.com/realms/certidevs

Al arrancar, Spring descarga el documento /.well-known/openid-configuration, localiza el jwks_uri y se cachea las claves públicas. Cualquier JWT presentado se valida contra esas claves de firma. Si las claves rotan, Spring refresca el caché automáticamente cuando encuentra un kid desconocido.

La cadena mínima de filtros queda así:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        return http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(AbstractHttpConfigurer::disable)
            .build();
    }
}

Con esta configuración, las peticiones con JWT válido pasan al controlador mientras que las peticiones sin token o con token inválido devuelven 401 Unauthorized de forma transparente.

Para APIs sin estado conviene desactivar CSRF y forzar SessionCreationPolicy.STATELESS. Spring Security no creará sesiones HTTP y cada petición se autentica de nuevo mediante el JWT.

Mapeo de claims a authorities

Por defecto, cada scope presente en el claim scope del JWT se convierte en una GrantedAuthority con prefijo SCOPE_. Sin embargo, muchos authorization servers embeben roles en claims personalizados. Keycloak los coloca en realm_access.roles, Auth0 los pública en un namespace personalizado y Microsoft Entra ID los devuelve en roles.

Para adaptar el mapeo se configura un JwtAuthenticationConverter:

import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;

@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter authorities = new JwtGrantedAuthoritiesConverter();
    authorities.setAuthoritiesClaimName("roles");
    authorities.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(authorities);
    return converter;
}

Con este converter, un JWT con "roles": ["admin", "gestor"] produce authorities ROLE_admin y ROLE_gestor. El método hasRole("admin") de Spring Security ya aplica el prefijo automáticamente, por lo que el resto de reglas queda limpio.

Validación de audience e issuer

Además de firma y expiración, es una buena práctica validar que el token está destinado a esta API concreta. El claim aud (audience) identifica para qué servicio se emitió el token; si no se valida, un token robado de otro servicio podría reutilizarse en este.

import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

@Bean
JwtDecoder jwtDecoder(OAuth2ResourceServerProperties props) {
    NimbusJwtDecoder decoder = NimbusJwtDecoder
        .withIssuerLocation(props.getJwt().getIssuerUri()).build();

    OAuth2TokenValidator<Jwt> audience = new AudienceValidator("certidevs-api");
    decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
        JwtValidators.createDefault(), audience));
    return decoder;
}

La clase AudienceValidator es una implementación propia que comprueba si la lista de audiences del JWT contiene el identificador esperado. Así la API rechaza tokens emitidos para otras audiencias, aunque la firma sea válida. El mismo patrón sirve para validar claims personalizados como tenant_id o niveles de suscripción.

Las validaciones adicionales componen con DelegatingOAuth2TokenValidator, que aplica todas en cadena. Cualquier fallo se traduce en un 401 con cabecera WWW-Authenticate según el estándar RFC 6750.

Caso B2B: API multi-tenant en una plataforma sanitaria

Una plataforma sanitaria española expone una API REST que consumen hospitales privados y clínicas concertadas. Cada cliente autentica a sus profesionales contra su propia instancia de Microsoft Entra ID, y el resource server valida el JWT recibido verificando firma, expiración y un claim propio tenant_id que identifica al hospital emisor. Con DelegatingOAuth2TokenValidator combinan validaciones estándar con la comprobación de que el token provenga de un tenant contratado. El diseño permite onboardear nuevos clientes sin tocar código, únicamente añadiendo su tenant a la lista autorizada en un ConfigMap de Kubernetes.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Boot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Spring Boot

Explora más contenido relacionado con Spring Boot y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Configurar Spring Security como OAuth2 Resource Server, validar JWT con issuer-uri, mapear claims a authorities y añadir validaciones personalizadas de audience y claims propios.