Validación de JWT de AWS Cognito en Spring Boot con Kotlin y Gradle
Fecha de publicación: 2025-01-25
En este tutorial aprenderás cómo configurar Spring Boot con Kotlin y Gradle para validar tokens JWT de AWS Cognito y proteger tus endpoints. Nos enfocaremos únicamente en la validación de tokens, asumiendo que el login se maneja desde el frontend o una aplicación externa.
Esta implementación es ideal cuando:
- Tu frontend maneja el login con Cognito directamente
- Tienes una aplicación móvil que ya obtiene tokens de Cognito
- Quieres un backend stateless que solo valide y proteja endpoints
- Necesitas una configuración simple y minimalista
¡Empecemos con la implementación!
🛠️ Paso 1: Configuración inicial del proyecto
Primero, agrega las dependencias necesarias en tu build.gradle.kts
:
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")
// Para Kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}
☁️ Paso 2: Configuración de AWS Cognito
Configura Cognito en tu application.yml
:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX
Nota: Reemplaza us-east-1_XXXXXXXXX
con el ID real de tu User Pool de Cognito. Puedes encontrarlo en la consola de AWS Cognito en la sección "User pools".
🔐 Paso 3: Configuración de Spring Security
Crea la configuración de seguridad para validar JWT tokens de Cognito:
@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 esta configuración simple, Spring Boot automáticamente:
- Descarga las claves públicas de Cognito
- Valida la firma de los JWT tokens
- Verifica que el token no haya expirado
- Extrae los claims del usuario del token
👥 Alternativa: Configuración con Roles
Si necesitas autorización basada en roles, puedes configurar Spring Security para usar los grupos de Cognito como roles de autorización:
@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 authenticationConverter = JwtAuthenticationConverter()
authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter)
return authenticationConverter
}
}
Explicación del código:@EnableMethodSecurity
: Habilita anotaciones como @PreAuthorize
hasRole("ADMIN")
: Solo usuarios con rol ADMIN pueden accederhasAnyRole("USER", "ADMIN")
: Usuarios con cualquiera de estos rolescognito:groups
: Claim del JWT que contiene los grupos del usuarioROLE_
: Prefijo automático que agrega Spring Security
Configuración en AWS Cognito
Para usar roles, necesitas configurar grupos en tu User Pool de Cognito:
# Crear grupos en Cognito usando AWS CLI
aws cognito-idp create-group \
--group-name "ADMIN" \
--user-pool-id us-east-1_XXXXXXXXX \
--description "Administrators"
aws cognito-idp create-group \
--group-name "USER" \
--user-pool-id us-east-1_XXXXXXXXX \
--description "Regular users"
# Asignar 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"
🛡️ Paso 4: Endpoint protegido de ejemplo
Crea endpoints para mostrar diferentes niveles de acceso: públicos, autenticados y con roles específicos:
@RestController
@RequestMapping("/api/public")
class PublicController {
@GetMapping("/health")
fun healthCheck(): ResponseEntity<Map<String, String>> {
return ResponseEntity.ok(mapOf(
"status" to "OK",
"message" to "Endpoint público - no requiere autenticación",
"timestamp" to Instant.now().toString()
))
}
}
@RestController
@RequestMapping("/api/protected")
class ProtectedController {
@GetMapping("/profile")
fun getUserProfile(authentication: JwtAuthenticationToken): ResponseEntity<Map<String, Any>> {
val jwt = authentication.token
return ResponseEntity.ok(mapOf(
"userId" to jwt.getClaimAsString("sub"),
"email" to jwt.getClaimAsString("email"),
"username" to jwt.getClaimAsString("cognito:username"),
"groups" to jwt.getClaimAsStringList("cognito:groups"),
"tokenType" to jwt.getClaimAsString("token_use"),
"issuedAt" to jwt.issuedAt,
"expiresAt" to jwt.expiresAt
))
}
}
@RestController
@RequestMapping("/api/admin")
class AdminController {
@GetMapping("/dashboard")
@PreAuthorize("hasRole('ADMIN')")
fun adminDashboard(): ResponseEntity<Map<String, String>> {
return ResponseEntity.ok(mapOf(
"message" to "Panel de administración - solo ADMIN",
"timestamp" to Instant.now().toString()
))
}
@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
fun getAllUsers(): ResponseEntity<Map<String, Any>> {
return ResponseEntity.ok(mapOf(
"users" to listOf("user1@example.com", "user2@example.com"),
"total" to 2,
"requiredRole" to "ADMIN"
))
}
}
@RestController
@RequestMapping("/api/user")
class UserController {
@GetMapping("/data")
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
fun getUserData(authentication: JwtAuthenticationToken): ResponseEntity<Map<String, Any>> {
val jwt = authentication.token
return ResponseEntity.ok(mapOf(
"data" to "Datos del usuario",
"allowedRoles" to listOf("USER", "ADMIN"),
"userGroups" to jwt.getClaimAsStringList("cognito:groups")
))
}
}
🧪 Paso 5: Prueba tu implementación
Para probar tu implementación necesitas un token JWT válido de Cognito. Puedes obtenerlo de varias formas:
Opción 1: Usar AWS CLI
# Instalar AWS CLI y configurar credenciales
aws configure
# Autenticar usuario y obtener tokens
aws cognito-idp initiate-auth \
--auth-flow USER_PASSWORD_AUTH \
--client-id your-client-id \
--auth-parameters USERNAME=test@example.com,PASSWORD=TempPassword123!
Opción 2: Probar con curl
# 1. Endpoint público (sin token)
curl -X GET http://localhost:8080/api/public/health
# 2. Endpoint protegido (requiere token válido)
curl -X GET http://localhost:8080/api/protected/profile \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsIn..."
# 3. Endpoint para usuarios (requiere rol USER o ADMIN)
curl -X GET http://localhost:8080/api/user/data \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsIn..."
# 4. Endpoint solo para administradores (requiere rol ADMIN)
curl -X GET http://localhost:8080/api/admin/dashboard \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsIn..."
# 5. Listar usuarios (solo ADMIN)
curl -X GET http://localhost:8080/api/admin/users \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsIn..."
Respuestas esperadas:
// Sin token en endpoint protegido:
HTTP 401 Unauthorized
// Con token válido pero sin rol requerido:
HTTP 403 Forbidden
{
"error": "Access Denied"
}
// Con token y rol correcto:
HTTP 200 OK
{
"userId": "12345-abcde-67890",
"email": "test@example.com",
"groups": ["ADMIN"],
"data": "Información del endpoint"
}
⚙️ Paso 6: Configuración avanzada (opcional)
Para una implementación más robusta, puedes agregar configuraciones adicionales:
// Imports necesarios:
// import java.time.Instant
// import javax.servlet.http.HttpServletResponse
// import org.springframework.security.web.access.AccessDeniedHandler
// import org.springframework.security.web.AuthenticationEntryPoint
@Configuration
class CognitoAdvancedConfig {
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOriginPatterns = listOf("*")
configuration.allowedMethods = listOf("*")
configuration.allowedHeaders = listOf("*")
configuration.allowCredentials = true
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", configuration)
return source
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val authoritiesConverter = JwtGrantedAuthoritiesConverter()
authoritiesConverter.setAuthorityPrefix("ROLE_")
authoritiesConverter.setAuthoritiesClaimName("cognito:groups")
val authenticationConverter = JwtAuthenticationConverter()
authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter)
// Opcional: personalizar el nombre del principal
authenticationConverter.setPrincipalClaimName("email")
return authenticationConverter
}
// Opcional: Manejo personalizado de errores de autorización
@Bean
fun accessDeniedHandler(): AccessDeniedHandler {
return AccessDeniedHandler { _, response, _ ->
response.status = HttpServletResponse.SC_FORBIDDEN
response.contentType = "application/json"
response.writer.write("""
{
"error": "Access Denied",
"message": "No tienes permisos para acceder a este recurso",
"timestamp": "${Instant.now()}"
}
""".trimIndent())
}
}
// Opcional: Manejo personalizado de errores de autenticación
@Bean
fun authenticationEntryPoint(): AuthenticationEntryPoint {
return AuthenticationEntryPoint { _, response, _ ->
response.status = HttpServletResponse.SC_UNAUTHORIZED
response.contentType = "application/json"
response.writer.write("""
{
"error": "Unauthorized",
"message": "Token JWT requerido",
"timestamp": "${Instant.now()}"
}
""".trimIndent())
}
}
}
🐛 Debugging: Problemas comunes con roles
Si tienes problemas con la autorización basada en roles, verifica:
// Endpoint para debug: ver qué authorities tiene el usuario
@GetMapping("/debug/authorities")
fun debugAuthorities(authentication: JwtAuthenticationToken): ResponseEntity<Map<String, Any>> {
val jwt = authentication.token
return ResponseEntity.ok(mapOf(
"principal" to authentication.name,
"authorities" to authentication.authorities.map { it.authority },
"groups" to jwt.getClaimAsStringList("cognito:groups"),
"allClaims" to jwt.claims
))
}
Checklist de problemas comunes:- ¿El usuario está asignado al grupo correcto en Cognito?
- ¿El claim
cognito:groups
aparece en el JWT? - ¿El prefijo
ROLE_
se está agregando correctamente? - ¿El nombre del grupo coincide exactamente? (case-sensitive)
- ¿Habilitaste
@EnableMethodSecurity
para usar @PreAuthorize
?
✅ Ventajas de esta implementación
- Simplicidad: Solo unas pocas líneas de configuración
- Seguridad automática: Validación JWT sin código manual
- Stateless: Perfecto para microservicios y aplicaciones distribuidas
- Integración nativa: Spring Security maneja todo automáticamente
- Separación de responsabilidades: Backend solo valida, frontend maneja login
- Escalabilidad: Sin gestión de sesiones en el servidor
- Compatibilidad: Funciona con cualquier cliente que envíe JWT tokens
🚀 Próximos pasos
Una vez que tengas la validación de tokens y roles funcionando, puedes explorar:
- Implementar roles jerárquicos (admin → user → guest)
- Agregar autorización a nivel de método con expresiones SpEL
- Configurar grupos dinámicos usando Lambda triggers en Cognito
- Implementar logging de acceso y auditoría de roles
- Agregar validaciones de permisos más granulares por endpoint
- Configurar cache de roles para mejor performance
- Implementar autorización condicional basada en atributos del usuario
📌 Conclusión
Esta implementación minimalista de validación JWT con AWS Cognito y Spring Boot te proporciona una base sólida y segura para proteger tus endpoints. Con muy poca configuración obtienes validación automática y robusta de tokens.
La separación de responsabilidades es clave: tu backend se enfoca únicamente en validar tokens y proteger recursos, mientras que la autenticación puede manejarse desde el frontend, aplicaciones móviles o servicios externos.