Validación de JWT de AWS Cognito en Spring Boot con Kotlin y Gradle

Jorge SaavedraJorge Saavedra
·20 de agosto, 2025·6 min de lectura
spring-bootkotlinawscognitojwtseguridadgradle
En este tutorial vas a configurar Spring Boot para validar tokens JWT de AWS Cognito y proteger tus endpoints. Nos enfocamos únicamente en la validación: el inicio de sesión lo gestiona el frontend o una aplicación externa. Tu backend es stateless y solo se encarga de verificar que el token sea legítimo.
Esta configuración es ideal cuando:
  • Tu frontend gestiona el login con Cognito directamente
  • Tienes una app móvil que ya obtiene tokens de Cognito
  • Quieres un backend stateless que solo valide y proteja endpoints

Dependencias

En tu build.gradle.kts:
kotlin
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.security:spring-security-oauth2-resource-server")
    implementation("org.springframework.security:spring-security-oauth2-jose")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
}

¿Qué hace cada dependencia?

  • spring-security-oauth2-resource-server: convierte tu app en un Resource Server que valida tokens
  • spring-security-oauth2-jose: maneja la verificación criptográfica de JWT (firma RS256, claves públicas)

Configurar Cognito

En application.yml, apunta al issuer URI de tu User Pool:
yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX
Reemplaza us-east-1_XXXXXXXXX con el ID de tu User Pool (lo encuentras en la consola de AWS Cognito). Con esto, Spring descarga automáticamente las claves públicas de Cognito para verificar las firmas.

Security config básica

La configuración mínima para validar JWT:
SecurityConfig.kt
@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        return http
            .csrf { it.disable() }
            .authorizeHttpRequests { auth ->
                auth
                    .requestMatchers("/api/public/**").permitAll()
                    .anyRequest().authenticated()
            }
            .oauth2ResourceServer { oauth2 ->
                oauth2.jwt()
            }
            .sessionManagement {
                it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            }
            .build()
    }
}
Con esto, Spring Boot automáticamente descarga las claves públicas de Cognito, valida la firma del JWT, verifica que no haya expirado, y extrae los claims del usuario. Todo sin una sola línea de código de validación manual.

Agregar autorización por roles

Si necesitas controlar acceso por grupos de Cognito, necesitas unJwtAuthenticationConverter que mapee el claim cognito:groups a roles de Spring Security:
SecurityConfigWithRoles.kt
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfigWithRoles {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        return http
            .csrf { it.disable() }
            .authorizeHttpRequests { auth ->
                auth
                    .requestMatchers("/api/public/**").permitAll()
                    .requestMatchers("/api/admin/**").hasRole("ADMIN")
                    .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                    .anyRequest().authenticated()
            }
            .oauth2ResourceServer { oauth2 ->
                oauth2.jwt { jwt ->
                    jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
                }
            }
            .sessionManagement {
                it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            }
            .build()
    }

    @Bean
    fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
        val authoritiesConverter = JwtGrantedAuthoritiesConverter()
        authoritiesConverter.setAuthorityPrefix("ROLE_")
        authoritiesConverter.setAuthoritiesClaimName("cognito:groups")

        val converter = JwtAuthenticationConverter()
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter)
        return converter
    }
}
El JwtGrantedAuthoritiesConverter lee el claim cognito:groups del token y lo convierte en authorities con el prefijo ROLE_. Así, un usuario en el grupo "ADMIN" de Cognito obtiene la authority ROLE_ADMIN en Spring Security.

Crear los grupos en Cognito

bash
# Crear grupos
aws cognito-idp create-group \
    --group-name "ADMIN" \
    --user-pool-id us-east-1_XXXXXXXXX

aws cognito-idp create-group \
    --group-name "USER" \
    --user-pool-id us-east-1_XXXXXXXXX

# Asignar un usuario a un grupo
aws cognito-idp admin-add-user-to-group \
    --user-pool-id us-east-1_XXXXXXXXX \
    --username "test@example.com" \
    --group-name "ADMIN"

Endpoints protegidos

Controllers.kt
@RestController
@RequestMapping("/api/public")
class PublicController {

    @GetMapping("/health")
    fun healthCheck() = mapOf(
        "status" to "OK",
        "timestamp" to Instant.now().toString()
    )
}

@RestController
@RequestMapping("/api/protected")
class ProtectedController {

    @GetMapping("/profile")
    fun getUserProfile(auth: JwtAuthenticationToken): Map<String, Any?> {
        val jwt = auth.token
        return mapOf(
            "userId" to jwt.getClaimAsString("sub"),
            "email" to jwt.getClaimAsString("email"),
            "groups" to jwt.getClaimAsStringList("cognito:groups")
        )
    }
}

@RestController
@RequestMapping("/api/admin")
class AdminController {

    @GetMapping("/dashboard")
    @PreAuthorize("hasRole('ADMIN')")
    fun adminDashboard() = mapOf(
        "message" to "Solo ADMIN puede ver esto",
        "timestamp" to Instant.now().toString()
    )
}

Probar con curl

bash
# Endpoint público (sin token)
curl http://localhost:8080/api/public/health

# Endpoint protegido (requiere token)
curl http://localhost:8080/api/protected/profile \
  -H "Authorization: Bearer <tu-token-jwt>"

# Endpoint admin (requiere rol ADMIN)
curl http://localhost:8080/api/admin/dashboard \
  -H "Authorization: Bearer <tu-token-jwt>"
Las respuestas esperadas:
text
Sin token en endpoint protegido:  HTTP 401 Unauthorized
Con token sin rol requerido:      HTTP 403 Forbidden
Con token y rol correcto:         HTTP 200 OK

Debugging

Si los roles no funcionan, agrega un endpoint temporal para inspeccionar qué authorities tiene el usuario:
kotlin
@GetMapping("/debug/authorities")
fun debug(auth: JwtAuthenticationToken) = mapOf(
    "authorities" to auth.authorities.map { it.authority },
    "groups" to auth.token.getClaimAsStringList("cognito:groups"),
    "allClaims" to auth.token.claims
)

Checklist de problemas comunes

  • ¿El usuario está asignado al grupo correcto en Cognito?
  • ¿El claim cognito:groups aparece en el JWT?
  • ¿El nombre del grupo coincide exactamente? (case-sensitive)
  • ¿Habilitaste @EnableMethodSecurity para usar @PreAuthorize?

En resumen

Con tres dependencias, una línea en application.yml y una clase de configuración, tu backend valida tokens JWT de AWS Cognito automáticamente. Agregar roles es cuestión de un JwtAuthenticationConverter que mapee cognito:groups a authorities de Spring Security.
Tu backend no maneja sesiones, no almacena estado, y no sabe nada del login. Solo valida lo que le llega en el header Authorization. Esa separación de responsabilidades es lo que hace que esta arquitectura escale.

Posts que podrian interesarte