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 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
🛠️ 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_XXXXXXXXXNota: 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@PreAuthorizehasRole("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:
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:groupsaparece en el JWT? - ¿El prefijo
ROLE_se está agregando correctamente? - ¿El nombre del grupo coincide exactamente? (case-sensitive)
- ¿Habilitaste
@EnableMethodSecuritypara 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.


