Autenticación ≠ Autorización
JSON Web Token. Un token firmado que el servidor de identidad (como AWS Cognito) le entrega al cliente después de un login exitoso. Contiene claims (datos) sobre el usuario y va firmado con una llave privada, así que el backend puede confiar en él sin consultar una base de datos.
401 Unauthorized: no sé quién eres. No mandaste token, está vencido o mal firmado.403 Forbidden: sé perfectamente quién eres, y aun así no puedes. El orden importa: primero siempre se resuelve la autenticación, y solo si pasa se evalúa la autorización.El orden de los dos controles: primero quién, después qué
La regla que hay que memorizar
401 = falló la autenticación (no sé quién eres). 403 = falló la autorización (sé quién eres, pero no puedes). Si alguna vez ves un 403, olvídate del login: tu token estaba perfecto. El problema es de permisos.La autorización tiene dos formas
- Por roles (jerarquía): ¿puedes hacer este tipo de acción en general? Por ejemplo, borrar cuentas de usuario, ocultar contenido reportado.
- Por propiedad (segmentación): ¿es tuya esta fila en particular? Por ejemplo, esta nota, este ticket, este pedido.
Los dos ejes: jerarquía (rol) y segmentación (propiedad)
Ojo con la palabra jerarquía
ADMIN pueda todo. Es simplemente otro rol con otro llavero. Una moderadora puede borrar cuentas, pero quizás no puede escribir notas; y un USER escribe notas, pero no toca cuentas ajenas. Los roles cortan en los dos sentidos, no solo hacia arriba.Eje 1 · Autorización por roles (jerarquía)
Uno de los tokens que emite Cognito. El claim cognito:groups viaja en el access_token, no en el id_token. Usar el token equivocado es una de las causas más comunes de que la autorización por roles no funcione.
El viaje de un rol: de Cognito a hasRole()
cognito:groups: es un claim inventado por AWS. Hay que enseñárselo con un JwtAuthenticationConverterComponente de Spring Security que traduce los claims de un JWT en authorities (permisos) que Spring entiende. Aquí lo usamos para convertir cada grupo de Cognito en una authority con el prefijo ROLE_.
// The heart of role-based authorization
private fun cognitoGroupsConverter(): JwtAuthenticationConverter {
val converter = JwtAuthenticationConverter()
converter.setJwtGrantedAuthoritiesConverter { jwt ->
val groups = jwt.getClaimAsStringList("cognito:groups") ?: emptyList()
groups.map { SimpleGrantedAuthority("ROLE_$it") } // ADMIN -> ROLE_ADMIN
}
return converter
}auth.requestMatchers(POST, "/notes").hasRole("USER") // any signed-in user writes notes
auth.requestMatchers(DELETE, "/notes/**").hasRole("USER") // ...ownership is checked later, in the service
auth.requestMatchers(DELETE, "/users/**").hasRole("ADMIN") // only a moderator removes accountsLos 3 detalles que todos se equivocan
hasRole("ADMIN") busca la authority ROLE_ADMIN: por eso el converter escribe ROLE_ a mano.2. El grupo en Cognito se llama
ADMIN, no ROLE_ADMIN. El prefijo lo pone el converter, no AWS.3. Usa el
access_token, no el id_token: el claim cognito:groups viaja en el access token.Eje 2 · Autorización por propiedad (segmentación)
USER), pero no tiene forma de saber de quién es la nota número 12. Ese dato está en tu base de datos, y Spring nunca la miró.¿Esta fila es tuya? La comparación que Spring no puede hacer
fun deleteNote(noteId: Long, username: String) {
val note = noteRepository.findById(noteId)
.orElseThrow { NoteNotFoundException(noteId) }
// The "ownership" 403: right role, but this row is NOT yours
if (note.username != username) {
throw NotYourNoteException("Note $noteId belongs to ${note.username}")
}
noteRepository.delete(note)
}username que le pasamos? Del token ya verificado, nunca del body de la petición:@DeleteMapping("/notes/{id}")
fun delete(@PathVariable id: Long, @AuthenticationPrincipal jwt: Jwt) =
noteService.deleteNote(id, jwt.getClaimAsString("username"))La regla de oro
body. Solo puede salir de un claim del JWT firmado por AWS: no se puede falsificar sin la llave privada de Amazon. Piensa en un endpoint GET /notes/me: no recibe ningún parámetro y aun así sabe de quién son las notas. El “me” lo define el token, no el cliente.Cómo coexisten: los dos 403
403, pero por razones completamente diferentes.El pipeline: un 401 y dos 403 distintos
La distinción que separa un backend seguro de uno inseguro
403, pero por razones completamente diferentes. Si solo implementas la Capa 1 (el rol), cualquier USER puede borrar las notas de cualquier otro USER: tienen el mismo rol. La Capa 2 (la propiedad) es la que te protege dentro del mismo rol.| Petición | Token | Resultado | Por qué |
|---|---|---|---|
| GET /notes/public | sin token | 200 | Endpoint público (permitAll) |
| POST /notes | sin token | 401 | AuthN: no sé quién eres |
| DELETE /users/5 | USER | 403 | AuthZ rol: solo un moderador |
| DELETE /users/5 | ADMIN | 204 | Rol correcto |
| DELETE /notes/12 (de ana) | USER · beto | 403 | AuthZ propiedad: no es tuya |
| DELETE /notes/12 (de ana) | USER · ana | 204 | Rol y dueño correctos |
Petición
Token
Resultado
Por qué
Petición
Token
Resultado
Por qué
Petición
Token
Resultado
Por qué
Petición
Token
Resultado
Por qué
Petición
Token
Resultado
Por qué
Petición
Token
Resultado
Por qué
El reparto: Cognito ↔ Spring
Quién hace qué, con el mismo token
Cognito
La recepción: verifica identidades y reparte llaves
- Guarda usuarios y contraseñas
- Guarda los grupos (los roles)
- Autentica y emite el JWT firmado
- Mete
cognito:groupsyusernameen el token
Spring
Las cerraduras: decide qué abre cada llave
- Valida el JWT (firma, emisor, expiración)
- Traduce
cognito:groupsaROLE_* - Aplica la autorización por rol (Capa 1)
- Te entrega el token para tu lógica de propiedad (Capa 2)
| Claim del JWT | Responde | Quién lo lee | Eje |
|---|---|---|---|
| cognito:groups | ¿Qué puedes hacer? (rol) | SecurityConfig | Jerarquía |
| username / sub | ¿Quién eres? (dueño) | Controller / Service | Segmentación |
Claim del JWT
Responde
Quién lo lee
Eje
Claim del JWT
Responde
Quién lo lee
Eje
Por qué esto es tan poderoso
Resumen para llevar
| Pregunta | Respuesta |
|---|---|
| ¿Autenticación o autorización? | AuthN = quién eres (401). AuthZ = qué puedes (403). |
| ¿Las dos formas de autorizar? | Por rol (jerarquía) y por propiedad (segmentación). |
| ¿Dónde viven los roles? | En Cognito (grupos), no en tu base de datos. |
| ¿Cómo llegan al backend? | En cognito:groups del access_token. |
| ¿Quién los traduce a roles Spring? | El JwtAuthenticationConverter → prefijo ROLE_. |
| ¿El ADMIN puede todo? | No. Es otro rol con otro llavero. Cortan en dos sentidos. |
| ¿De dónde sale el dueño de una fila? | Del claim username del JWT, nunca del body. |
| ¿Los dos 403 son lo mismo? | No. Uno es por rol (Spring); el otro por propiedad (tu service). |
| ¿Cuántas tablas agregan los roles? | Cero. Ni users, ni roles, ni user_roles. |