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

Fecha de publicación: 2025-01-25
Spring Boot y AWS Cognito
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 inicio de sesión se gestiona desde el frontend o una aplicación externa.
Esta implementación es ideal cuando:
  • Tu frontend gestiona el inicio de sesión 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:
kotlin
1dependencies {
2    implementation("org.springframework.boot:spring-boot-starter-web")
3    implementation("org.springframework.boot:spring-boot-starter-security")
4    implementation("org.springframework.security:spring-security-oauth2-resource-server")
5    implementation("org.springframework.security:spring-security-oauth2-jose")
6    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
7    
8    // Para Kotlin
9    implementation("org.jetbrains.kotlin:kotlin-reflect")
10    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
11}

☁️ Paso 2: Configuración de AWS Cognito

Configura Cognito en tu application.yml:
yaml
1spring:
2  security:
3    oauth2:
4      resourceserver:
5        jwt:
6          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:
kotlin
1@Configuration
2@EnableWebSecurity
3class SecurityConfig {
4
5    @Bean
6    fun filterChain(http: HttpSecurity): SecurityFilterChain {
7        return http
8            .csrf { it.disable() }
9            .authorizeHttpRequests { auth ->
10                auth
11                    .requestMatchers("/api/public/**").permitAll()
12                    .anyRequest().authenticated()
13            }
14            .oauth2ResourceServer { oauth2 ->
15                oauth2.jwt()
16            }
17            .sessionManagement { 
18                it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) 
19            }
20            .build()
21    }
22}
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:
kotlin
1@Configuration
2@EnableWebSecurity
3@EnableMethodSecurity
4class SecurityConfigWithRoles {
5
6    @Bean
7    fun filterChain(http: HttpSecurity): SecurityFilterChain {
8        return http
9            .csrf { it.disable() }
10            .authorizeHttpRequests { auth ->
11                auth
12                    .requestMatchers("/api/public/**").permitAll()
13                    .requestMatchers("/api/admin/**").hasRole("ADMIN")
14                    .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
15                    .anyRequest().authenticated()
16            }
17            .oauth2ResourceServer { oauth2 ->
18                oauth2.jwt { jwt ->
19                    jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
20                }
21            }
22            .sessionManagement { 
23                it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) 
24            }
25            .build()
26    }
27
28    @Bean
29    fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
30        val authoritiesConverter = JwtGrantedAuthoritiesConverter()
31        authoritiesConverter.setAuthorityPrefix("ROLE_")
32        authoritiesConverter.setAuthoritiesClaimName("cognito:groups")
33
34        val authenticationConverter = JwtAuthenticationConverter()
35        authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter)
36        return authenticationConverter
37    }
38}
Explicación del código:
  • @EnableMethodSecurity: Habilita anotaciones como @PreAuthorize
  • hasRole("ADMIN"): Solo usuarios con rol ADMIN pueden acceder
  • hasAnyRole("USER", "ADMIN"): Usuarios con cualquiera de estos roles
  • cognito:groups: Claim del JWT que contiene los grupos del usuario
  • ROLE_: Prefijo automático que agrega Spring Security

Configuración en AWS Cognito

Para usar roles, necesitas configurar grupos en tu User Pool de Cognito:
bash
1# Crear grupos en Cognito usando AWS CLI
2aws cognito-idp create-group \
3    --group-name "ADMIN" \
4    --user-pool-id us-east-1_XXXXXXXXX \
5    --description "Administrators"
6
7aws cognito-idp create-group \
8    --group-name "USER" \
9    --user-pool-id us-east-1_XXXXXXXXX \
10    --description "Regular users"
11
12# Asignar usuario a un grupo
13aws cognito-idp admin-add-user-to-group \
14    --user-pool-id us-east-1_XXXXXXXXX \
15    --username "test@example.com" \
16    --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:
kotlin
1@RestController
2@RequestMapping("/api/public")
3class PublicController {
4
5    @GetMapping("/health")
6    fun healthCheck(): ResponseEntity<Map<String, String>> {
7        return ResponseEntity.ok(mapOf(
8            "status" to "OK",
9            "message" to "Endpoint público - no requiere autenticación",
10            "timestamp" to Instant.now().toString()
11        ))
12    }
13}
14
15@RestController
16@RequestMapping("/api/protected")
17class ProtectedController {
18
19    @GetMapping("/profile")
20    fun getUserProfile(authentication: JwtAuthenticationToken): ResponseEntity<Map<String, Any>> {
21        val jwt = authentication.token
22        
23        return ResponseEntity.ok(mapOf(
24            "userId" to jwt.getClaimAsString("sub"),
25            "email" to jwt.getClaimAsString("email"),
26            "username" to jwt.getClaimAsString("cognito:username"),
27            "groups" to jwt.getClaimAsStringList("cognito:groups"),
28            "tokenType" to jwt.getClaimAsString("token_use"),
29            "issuedAt" to jwt.issuedAt,
30            "expiresAt" to jwt.expiresAt
31        ))
32    }
33}
34
35@RestController
36@RequestMapping("/api/admin")
37class AdminController {
38
39    @GetMapping("/dashboard")
40    @PreAuthorize("hasRole('ADMIN')")
41    fun adminDashboard(): ResponseEntity<Map<String, String>> {
42        return ResponseEntity.ok(mapOf(
43            "message" to "Panel de administración - solo ADMIN",
44            "timestamp" to Instant.now().toString()
45        ))
46    }
47    
48    @GetMapping("/users")
49    @PreAuthorize("hasRole('ADMIN')")
50    fun getAllUsers(): ResponseEntity<Map<String, Any>> {
51        return ResponseEntity.ok(mapOf(
52            "users" to listOf("user1@example.com", "user2@example.com"),
53            "total" to 2,
54            "requiredRole" to "ADMIN"
55        ))
56    }
57}
58
59@RestController
60@RequestMapping("/api/user")
61class UserController {
62
63    @GetMapping("/data")
64    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
65    fun getUserData(authentication: JwtAuthenticationToken): ResponseEntity<Map<String, Any>> {
66        val jwt = authentication.token
67        return ResponseEntity.ok(mapOf(
68            "data" to "Datos del usuario",
69            "allowedRoles" to listOf("USER", "ADMIN"),
70            "userGroups" to jwt.getClaimAsStringList("cognito:groups")
71        ))
72    }
73}

🧪 Paso 5: Prueba tu implementación

Para probar tu implementación necesitas un token JWT válido de Cognito. Puedes obtenerlo de varias maneras:

Opción 1: Usar AWS CLI

bash
1# Instalar AWS CLI y configurar credenciales
2aws configure
3
4# Autenticar usuario y obtener tokens
5aws cognito-idp initiate-auth \
6    --auth-flow USER_PASSWORD_AUTH \
7    --client-id your-client-id \
8    --auth-parameters USERNAME=test@example.com,PASSWORD=TempPassword123!

Opción 2: Probar con curl

bash
1# 1. Endpoint público (sin token)
2curl -X GET http://localhost:8080/api/public/health
3
4# 2. Endpoint protegido (requiere token válido)
5curl -X GET http://localhost:8080/api/protected/profile \
6  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsIn..."
7
8# 3. Endpoint para usuarios (requiere rol USER o ADMIN)
9curl -X GET http://localhost:8080/api/user/data \
10  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsIn..."
11
12# 4. Endpoint solo para administradores (requiere rol ADMIN)
13curl -X GET http://localhost:8080/api/admin/dashboard \
14  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsIn..."
15
16# 5. Listar usuarios (solo ADMIN)
17curl -X GET http://localhost:8080/api/admin/users \
18  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsIn..."

Respuestas esperadas:

json
1// Sin token en endpoint protegido:
2HTTP 401 Unauthorized
3
4// Con token válido pero sin rol requerido:
5HTTP 403 Forbidden
6{
7  "error": "Access Denied"
8}
9
10// Con token y rol correcto:
11HTTP 200 OK
12{
13  "userId": "12345-abcde-67890",
14  "email": "test@example.com",
15  "groups": ["ADMIN"],
16  "data": "Información del endpoint"
17}

⚙️ Paso 6: Configuración avanzada (opcional)

Para una implementación más robusta, puedes agregar configuraciones adicionales:
kotlin
1// Imports necesarios:
2// import java.time.Instant
3// import javax.servlet.http.HttpServletResponse
4// import org.springframework.security.web.access.AccessDeniedHandler
5// import org.springframework.security.web.AuthenticationEntryPoint
6
7@Configuration
8class CognitoAdvancedConfig {
9
10    @Bean
11    fun corsConfigurationSource(): CorsConfigurationSource {
12        val configuration = CorsConfiguration()
13        configuration.allowedOriginPatterns = listOf("*")
14        configuration.allowedMethods = listOf("*")
15        configuration.allowedHeaders = listOf("*")
16        configuration.allowCredentials = true
17        
18        val source = UrlBasedCorsConfigurationSource()
19        source.registerCorsConfiguration("/**", configuration)
20        return source
21    }
22
23    @Bean
24    fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
25        val authoritiesConverter = JwtGrantedAuthoritiesConverter()
26        authoritiesConverter.setAuthorityPrefix("ROLE_")
27        authoritiesConverter.setAuthoritiesClaimName("cognito:groups")
28
29        val authenticationConverter = JwtAuthenticationConverter()
30        authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter)
31        
32        // Opcional: personalizar el nombre del principal
33        authenticationConverter.setPrincipalClaimName("email")
34        
35        return authenticationConverter
36    }
37
38    // Opcional: Manejo personalizado de errores de autorización
39    @Bean
40    fun accessDeniedHandler(): AccessDeniedHandler {
41        return AccessDeniedHandler { _, response, _ ->
42            response.status = HttpServletResponse.SC_FORBIDDEN
43            response.contentType = "application/json"
44            response.writer.write("""
45                {
46                    "error": "Access Denied",
47                    "message": "No tienes permisos para acceder a este recurso",
48                    "timestamp": "${Instant.now()}"
49                }
50            """.trimIndent())
51        }
52    }
53
54    // Opcional: Manejo personalizado de errores de autenticación
55    @Bean 
56    fun authenticationEntryPoint(): AuthenticationEntryPoint {
57        return AuthenticationEntryPoint { _, response, _ ->
58            response.status = HttpServletResponse.SC_UNAUTHORIZED
59            response.contentType = "application/json"
60            response.writer.write("""
61                {
62                    "error": "Unauthorized",
63                    "message": "Token JWT requerido",
64                    "timestamp": "${Instant.now()}"
65                }
66            """.trimIndent())
67        }
68    }
69}

🐛 Debugging: Problemas comunes con roles

Si tienes problemas con la autorización basada en roles, verifica:
kotlin
1// Endpoint para debug: ver qué authorities tiene el usuario
2@GetMapping("/debug/authorities")
3fun debugAuthorities(authentication: JwtAuthenticationToken): ResponseEntity<Map<String, Any>> {
4    val jwt = authentication.token
5    return ResponseEntity.ok(mapOf(
6        "principal" to authentication.name,
7        "authorities" to authentication.authorities.map { it.authority },
8        "groups" to jwt.getClaimAsStringList("cognito:groups"),
9        "allClaims" to jwt.claims
10    ))
11}
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 una 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 gestionarse desde el frontend, aplicaciones móviles o servicios externos.

Posts que podrian interesarte