Seguridad de microservicios con Spring Boot y OAuth2

Capítulo 6

Tiempo estimado de lectura: 10 minutos

+ Ejercicio

Seguridad basada en tokens: autenticación vs autorización

En una arquitectura de microservicios, la seguridad basada en tokens evita mantener sesión en el servidor y permite que cada petición lleve la identidad y permisos del cliente. En OAuth2, el token (normalmente un JWT) representa un conjunto de claims (datos firmados) que el backend valida para decidir si permite el acceso.

Autenticación

Responde a: ¿quién eres?. En APIs, suele materializarse como un token válido (firma correcta, no expirado, emisor/audiencia correctos). Si falla, la respuesta típica es 401 Unauthorized.

Autorización

Responde a: ¿qué puedes hacer?. Se basa en scopes (permisos orientados a acciones) y/o roles (perfiles). Si el usuario está autenticado pero no tiene permisos, la respuesta típica es 403 Forbidden.

Scopes vs roles (cuándo usar cada uno)

  • Scopes: ideales para APIs (por ejemplo, orders:read, orders:write). Se suelen mapear a autoridades con prefijo SCOPE_.
  • Roles: útiles cuando hay perfiles de negocio (por ejemplo, ADMIN, SUPPORT). En Spring Security suelen mapearse con prefijo ROLE_.

Recomendación práctica: usa scopes para proteger endpoints (contratos de API) y roles para reglas internas de administración o backoffice.

Configuración de Spring Security para APIs REST (Resource Server OAuth2)

En microservicios, lo más común es que cada servicio actúe como Resource Server: valida tokens emitidos por un Authorization Server (por ejemplo, Keycloak, Auth0, Okta, Cognito). El microservicio no “loguea” usuarios; solo valida y autoriza.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

Paso 1: dependencias

En Maven, añade Spring Security y el Resource Server con JWT:

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

Paso 2: configurar validación JWT (issuer o jwk-set-uri)

La forma más simple es configurar el issuer-uri (Spring descubrirá el endpoint de claves JWK):

spring:  security:    oauth2:      resourceserver:        jwt:          issuer-uri: https://auth.ejemplo.com/realms/mi-realm

Alternativa: configurar directamente el set de claves públicas:

spring:  security:    oauth2:      resourceserver:        jwt:          jwk-set-uri: https://auth.ejemplo.com/realms/mi-realm/protocol/openid-connect/certs

Paso 3: reglas por ruta (endpoints públicos y protegidos)

Define un SecurityFilterChain para APIs REST: deshabilita CSRF (si no usas cookies), configura stateless y reglas por ruta.

@Configuration@EnableWebSecuritypublic class SecurityConfig {  @Bean  SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {    http      .csrf(csrf -> csrf.disable())      .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))      .authorizeHttpRequests(auth -> auth        .requestMatchers("/actuator/health", "/actuator/info").permitAll()        .requestMatchers(HttpMethod.GET, "/api/orders/**").hasAuthority("SCOPE_orders:read")        .requestMatchers(HttpMethod.POST, "/api/orders/**").hasAuthority("SCOPE_orders:write")        .anyRequest().authenticated()      )      .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));    return http.build();  }}

Notas prácticas:

  • hasAuthority("SCOPE_x") es el patrón habitual para scopes.
  • Si usas roles, puedes aplicar hasRole("ADMIN") (internamente busca ROLE_ADMIN).
  • En microservicios, evita reglas demasiado “globales”; define permisos por recurso y método HTTP.

Paso 4: filtros y orden (qué ocurre en la cadena de seguridad)

En modo Resource Server con JWT, Spring agrega un filtro que:

  • Extrae el token del header Authorization: Bearer ....
  • Valida firma/expiración/issuer/audience según configuración.
  • Construye un Authentication con authorities (scopes/roles) para que las reglas de autorización funcionen.

Si necesitas lógica adicional (por ejemplo, correlación, logging seguro, bloqueo por IP, o validaciones extra), puedes añadir un filtro propio antes o después del filtro de autenticación. Ejemplo: un filtro que rechaza tokens sin kid (solo como demostración):

@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  http    .addFilterBefore(new OncePerRequestFilter() {      @Override      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)          throws ServletException, IOException {        // Ejemplo: no loguear el token; solo validar presencia de header        String auth = request.getHeader("Authorization");        if (auth != null && auth.startsWith("Bearer ") && auth.length() < 20) {          response.sendError(HttpServletResponse.SC_UNAUTHORIZED);          return;        }        filterChain.doFilter(request, response);      }    }, BearerTokenAuthenticationFilter.class)    .oauth2ResourceServer(oauth2 -> oauth2.jwt());  return http.build();}

Manejo de errores 401/403 en APIs (respuestas consistentes)

Para clientes (frontends, otros servicios), es clave diferenciar:

  • 401: falta token o token inválido/expirado.
  • 403: token válido pero sin permisos.

Puedes personalizar el cuerpo JSON de error con un AuthenticationEntryPoint (401) y un AccessDeniedHandler (403):

@BeanSecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  http    .csrf(csrf -> csrf.disable())    .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))    .exceptionHandling(ex -> ex      .authenticationEntryPoint((request, response, authException) -> {        response.setStatus(401);        response.setContentType("application/json");        response.getWriter().write("{\"error\":\"unauthorized\",\"message\":\"Token ausente o inválido\"}");      })      .accessDeniedHandler((request, response, accessDeniedException) -> {        response.setStatus(403);        response.setContentType("application/json");        response.getWriter().write("{\"error\":\"forbidden\",\"message\":\"Permisos insuficientes\"}");      })    )    .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())    .oauth2ResourceServer(oauth2 -> oauth2.jwt());  return http.build();}

Consejo: mantén mensajes genéricos (no reveles si el usuario existe, ni detalles de validación criptográfica).

Validación de JWT: qué validar y cómo endurecerlo

Validaciones mínimas recomendadas

  • Firma: usando las claves públicas del Authorization Server (JWK).
  • Expiración: claim exp.
  • Issuer: claim iss debe coincidir con tu issuer-uri.
  • Audience (si aplica): claim aud debe contener el identificador de tu API.

Spring valida automáticamente firma/exp/iss cuando configuras issuer-uri. Para validar aud, añade un validador:

@BeanJwtDecoder jwtDecoder(OAuth2ResourceServerProperties props) {  NimbusJwtDecoder decoder = JwtDecoders.fromIssuerLocation(props.getJwt().getIssuerUri());  OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(props.getJwt().getIssuerUri());  OAuth2TokenValidator<Jwt> audienceValidator = token -> {    List<String> aud = token.getAudience();    return aud != null && aud.contains("orders-api")      ? OAuth2TokenValidatorResult.success()      : OAuth2TokenValidatorResult.failure(new OAuth2Error("invalid_token", "Invalid audience", null));  };  decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator));  return decoder;}

Mapeo de claims a authorities (scopes/roles)

Según el proveedor, los scopes pueden venir en scope (string) o scp (lista). Los roles pueden venir en claims específicos (por ejemplo, realm_access.roles en Keycloak). Puedes personalizar el convertidor:

@BeanJwtAuthenticationConverter jwtAuthenticationConverter() {  JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();  scopes.setAuthorityPrefix("SCOPE_");  scopes.setAuthoritiesClaimName("scope");  JwtAuthenticationConverter converter = new JwtAuthenticationConverter();  converter.setJwtGrantedAuthoritiesConverter(jwt -> {    Collection<GrantedAuthority> authorities = new ArrayList<>(scopes.convert(jwt));    Map<String, Object> realmAccess = jwt.getClaim("realm_access");    if (realmAccess != null && realmAccess.get("roles") instanceof Collection<?> roles) {      for (Object r : roles) {        authorities.add(new SimpleGrantedAuthority("ROLE_" + r.toString()));      }    }    return authorities;  });  return converter;}

Y lo conectas al Resource Server:

.oauth2ResourceServer(oauth2 -> oauth2  .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())))

Rotación de claves (JWK) y disponibilidad

La rotación de claves es esencial: el Authorization Server cambia periódicamente las claves de firma. Con JWK, el token incluye un kid (Key ID) y el Resource Server descarga el set de claves públicas para validar.

Buenas prácticas

  • Usa JWK Set (no claves “hardcodeadas”).
  • Cache control: el decoder cachea claves; aun así, asegúrate de que el endpoint JWK sea altamente disponible.
  • Plan de rotación: durante un periodo, el Authorization Server debe publicar claves antiguas y nuevas para validar tokens emitidos antes del cambio.
  • Timeouts: configura timeouts de red razonables para evitar bloquear peticiones si el JWK endpoint está lento (especialmente en arranque).

Si tu entorno requiere control fino (proxy corporativo, timeouts, cache), considera configurar explícitamente el RestOperations usado por el decoder Nimbus (según versión de Spring Boot) o proveer un JwtDecoder con cliente HTTP ajustado.

Evitar exponer información sensible (prácticas concretas)

No loguear tokens ni claims sensibles

  • No imprimas el header Authorization en logs.
  • Evita loguear el JWT completo (contiene PII o permisos).
  • Si necesitas trazabilidad, loguea un identificador no sensible: sub (si no es PII), o un jti (token id) si existe.

Minimiza datos en el JWT

Un JWT no está cifrado por defecto; está firmado. Cualquiera que lo tenga puede decodificar su payload. Por tanto:

  • No incluyas datos personales innecesarios (email, teléfono, dirección).
  • Incluye solo lo necesario para autorización (scopes/roles) y trazabilidad (subject, tenant, etc.).

Mensajes de error y cabeceras

  • Respuestas 401/403 sin detalles internos (no revelar si la firma falló, si el usuario está deshabilitado, etc.).
  • Configura CORS de forma restrictiva si hay clientes browser.
  • Actuator: expón solo endpoints necesarios y protégelos (o al menos restringe por red).

Validación adicional de contexto

En escenarios multi-tenant o B2B, valida claims de contexto:

  • tenant_id o org presente y permitido.
  • Políticas por cliente (azp / authorized party) si tu proveedor lo emite.

Seguridad entre servicios (service-to-service)

Además de usuarios finales, los microservicios se llaman entre sí. Aquí necesitas identidad de servicio y permisos de servicio.

Estrategia 1: OAuth2 Client Credentials (recomendada)

Cada microservicio que llama a otro obtiene un token con client credentials (sin usuario) desde el Authorization Server. Ese token lleva scopes de servicio (por ejemplo, inventory:read).

Paso a paso (caller service):

  • Registrar un cliente (client_id/client_secret o mTLS) en el Authorization Server.
  • Configurar Spring como OAuth2 Client para obtener tokens.
  • Adjuntar el token en llamadas HTTP salientes.

Configuración típica (properties):

spring:  security:    oauth2:      client:        registration:          inventory-client:            provider: my-auth            client-id: inventory-caller            client-secret: ${INVENTORY_CALLER_SECRET}            authorization-grant-type: client_credentials            scope: inventory:read        provider:          my-auth:            token-uri: https://auth.ejemplo.com/realms/mi-realm/protocol/openid-connect/token

Si usas WebClient, puedes integrar un filtro que gestione el token automáticamente:

@BeanWebClient webClient(ClientRegistrationRepository clients, OAuth2AuthorizedClientService clientService) {  ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =    new ServletOAuth2AuthorizedClientExchangeFilterFunction(      new AuthorizedClientServiceOAuth2AuthorizedClientManager(clients, clientService));  oauth2.setDefaultClientRegistrationId("inventory-client");  return WebClient.builder().apply(oauth2.oauth2Configuration()).build();}

En el servicio destino, proteges endpoints con scopes de servicio igual que con usuarios.

Estrategia 2: Token exchange / propagación de identidad

Cuando un servicio A recibe una petición de usuario y llama a B, a veces necesitas que B conozca al usuario (auditoría, permisos finos). Opciones:

  • Propagar el token del usuario: simple, pero aumenta el acoplamiento y el alcance del token.
  • Token exchange: A intercambia el token del usuario por un token para B con audiencia específica y scopes mínimos. Requiere soporte del Authorization Server.

Regla práctica: si B solo necesita autorización por servicio, usa client credentials; si necesita identidad del usuario, prefiere token exchange con audiencia restringida.

Estrategia 3: mTLS entre servicios

mTLS autentica servicios a nivel de transporte. Puede combinarse con JWT (defensa en profundidad). Es útil en redes no totalmente confiables o cuando quieres identidad fuerte del servicio sin depender solo de secretos.

Gestión de secretos y configuración sensible

Qué no debe ir en el repositorio

  • client_secret, passwords, API keys.
  • Certificados privados, keystores con claves privadas.
  • Tokens de prueba.

Estrategias recomendadas

  • Variables de entorno: para secretos simples (por ejemplo, ${INVENTORY_CALLER_SECRET}).
  • Secret managers: Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager. Ideal para rotación, auditoría y control de acceso.
  • Kubernetes Secrets: útil, pero trata el secreto como material sensible (RBAC estricto, cifrado en etcd, evitar exponerlo en manifests).
  • Configuración por perfiles: separa application-dev.yml de application-prod.yml, pero sin secretos dentro.

Rotación de secretos (client credentials)

Planifica rotación sin downtime:

  • Permite dos secretos válidos temporalmente (old/new) en el Authorization Server.
  • Despliega servicios para usar el nuevo secreto.
  • Revoca el secreto antiguo.

Checklist rápido de endurecimiento

ÁreaAcción
JWTValidar iss/exp y aud; mapear scopes/roles de forma explícita
Errores401/403 consistentes, sin detalles internos
LogsNo loguear Authorization; evitar claims sensibles
Service-to-serviceClient credentials o token exchange; scopes mínimos
SecretosSecret manager/variables de entorno; rotación planificada

Ahora responde el ejercicio sobre el contenido:

En una API REST protegida como Resource Server con JWT, ¿en qué escenario es más apropiado responder con 403 en lugar de 401?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Un 401 indica falta de autenticación (token ausente o inválido). Un 403 se usa cuando la autenticación fue exitosa (token válido), pero la autorización falla por permisos insuficientes (scopes/roles).

Siguiente capítulo

Observabilidad en microservicios Spring Boot: logs, métricas y trazas

Arrow Right Icon
Portada de libro electrónico gratuitaMicroservicios con Spring Boot desde Cero
50%

Microservicios con Spring Boot desde Cero

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.