Autenticación vs. Autorización: los dos ejes que todo backend confunde

Jorge SaavedraJorge Saavedra
·14 de julio, 2026·10 min de lectura
seguridadautenticacionautorizacionspring-bootkotlinjwtcognitobuenas-practicas
Piensa en la última vez que llegaste a un hotel. En la recepción te pidieron tu cédula y una reserva a tu nombre: querían estar seguros de que tú eres tú. Cuando lo confirmaron, te dieron una llave. Esa llave no abre cualquier puerta: abre tu habitación y el gimnasio, pero no la habitación del vecino ni la oficina del gerente. Sin darte cuenta pasaste por dos controles completamente distintos, y esa distinción es exactamente la que separa un backend seguro de uno inseguro.
El primer control responde ¿quién eres?. El segundo responde ¿qué puedes hacer?. Son preguntas diferentes, se resuelven en momentos diferentes y, cuando fallan, devuelven errores diferentes. Mezclarlas es una de las causas más comunes de bugs de seguridad. En este post vamos a separarlas con calma, y después vamos a ver algo que casi nadie explica bien: que la autorización no es una sola cosa, sino que corta el sistema en dos ejes a la vez.

Autenticación ≠ Autorización

La autenticación (AuthN) verifica tu identidad. En un backend moderno significa: “este JWT
JWT

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.

que me mandaste, ¿es real, está firmado por quien dice, no está vencido?”
. Si la respuesta es no, el backend responde 401 Unauthorized: no sé quién eres. No mandaste token, está vencido o mal firmado.
La autorización (AuthZ) es el paso siguiente: una vez que sé quién eres, ¿tienes permiso para esta acción concreta?. Si no lo tienes, el backend responde 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é

Petición+ JWTAuthN¿Quién eres?¿token válido?AuthZ¿Qué puedes?¿tienes permiso?200adelante401no sé quién eres403sé quién eres, pero no

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

Aquí es donde casi todos se quedan cortos. La autorización no es una sola pregunta. Tiene dos formas que trabajan juntas y responden cosas distintas:
  • 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.
La mejor analogía es un concierto. Tu entrada prueba que pagaste: eso es autenticación. Tu pulsera VIP decide a qué zonas puedes entrar (general, VIP, backstage): eso es autorización por rol, y define el tipo de acceso. Y tu asiento asignado (fila H, número 12) es tuyo y de nadie más aunque haya mil personas con la misma pulsera: eso es autorización por propiedad. Dos personas pueden tener la misma pulsera y aun así no poder sentarse en el asiento de la otra.
Dicho de otra forma: la autorización corta el sistema en dos direcciones a la vez. Un eje vertical, la jerarquía, que decide qué clase de acción puedes hacer según tu rol. Y un eje horizontal, la segmentación, que decide sobre cuáles datos, según de quién son. Cada persona queda ubicada en ese plano por su rol y por lo que le pertenece.

Los dos ejes: jerarquía (rol) y segmentación (propiedad)

JERARQUÍA (rol)SEGMENTACIÓN (propiedad)ADMINmoderadorasubir = otro rol,otro llaveromismo rol, distinto dueño de SUS filasUSER · ana (tú)USER · betoUSER · caro

Ojo con la palabra jerarquía

“Jerarquía” no significa que 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)

Responde: ¿puedes hacer esta clase de operación?. El dato que la alimenta es el rol, y aquí viene una decisión de arquitectura que sorprende a mucha gente: en un stack con AWS Cognito, el rol no vive en tu base de datos. Vive en Cognito, como un grupo, y llega a tu backend dentro del token.
Vale la pena seguir el recorrido completo de un rol, porque cada salto es un lugar donde la gente se equivoca. Empieza como un grupo en Cognito, viaja dentro del access_token
access_token

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.

, y Spring lo traduce a algo que entiende.

El viaje de un rol: de Cognito a hasRole()

Grupo en CognitoADMINJWT (access_token)cognito:groups:[ADMIN]ConverterROLE_ADMINhasRole("ADMIN")
Spring, por defecto, no sabe qué es cognito:groups: es un claim inventado por AWS. Hay que enseñárselo con un JwtAuthenticationConverter
JwtAuthenticationConverter

Componente 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_.

:
SecurityConfig.kt
// 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
}
Y la protección por endpoint, declarativa, se aplica antes de que la petición toque tu código:
SecurityConfig.kt
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 accounts

Los 3 detalles que todos se equivocan

1. 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)

Ahora la otra pregunta: sé que puedes borrar notas en general, pero, ¿es tuya ESTA?. Y aquí está el detalle clave: Spring Security no puede responderla. Spring sabe qué eres (un 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ó.
Volvamos al concierto: el de seguridad en la entrada revisa tu pulsera (rol) y te deja pasar a la zona VIP. Pero él no tiene el mapa de asientos. Quien te dice “ese es mi asiento, no el tuyo” es la persona que ya está sentada, o el acomodador con la lista. Esa segunda verificación, la de propiedad, ocurre dentro, no en la puerta.

¿Esta fila es tuya? La comparación que Spring no puede hacer

Del token verificadojwt.username= "ana"Filas en TU base de datosNota #12username = "ana"Nota #40username = "beto"403Nota #71username = "caro"403row.username == jwt.username ?
Esa comparación la haces tú, en el service, contrastando el dueño de la fila contra el usuario del token:
NoteService.kt
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)
}
¿De dónde sale el username que le pasamos? Del token ya verificado, nunca del body de la petición:
NoteController.kt
@DeleteMapping("/notes/{id}")
fun delete(@PathVariable id: Long, @AuthenticationPrincipal jwt: Jwt) =
    noteService.deleteNote(id, jwt.getClaimAsString("username"))

La regla de oro

Un dato de identidad (quién es el dueño) jamás se acepta del 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

Una misma petición puede pasar el filtro de rol y aun así ser rechazada por propiedad. Son dos capas encadenadas, y las dos devuelven 403, pero por razones completamente diferentes.

El pipeline: un 401 y dos 403 distintos

Petición+ JWT¿Token válido?AuthN¿Rol correcto?Capa 1 · Spring¿Eres el dueño?Capa 2 · Service200 / 204adelante401no sé quién eres403 · rolno es tu tipo de acción403 · propiedadno es tu fila

La distinción que separa un backend seguro de uno inseguro

Los dos devuelven 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.
Puesto todo junto, así se comporta una misma ruta según quién la llama:

Petición

GET /notes/public

Token

sin token

Resultado

200

Por qué

Endpoint público (permitAll)

Petición

POST /notes

Token

sin token

Resultado

401

Por qué

AuthN: no sé quién eres

Petición

DELETE /users/5

Token

USER

Resultado

403

Por qué

AuthZ rol: solo un moderador

Petición

DELETE /users/5

Token

ADMIN

Resultado

204

Por qué

Rol correcto

Petición

DELETE /notes/12 (de ana)

Token

USER · beto

Resultado

403

Por qué

AuthZ propiedad: no es tuya

Petición

DELETE /notes/12 (de ana)

Token

USER · ana

Resultado

204

Por qué

Rol y dueño correctos

El reparto: Cognito ↔ Spring

Todo esto funciona porque cada pieza tiene un trabajo claro. Tu backend no guarda usuarios ni roles: confía en un papel firmado por AWS. Cognito es la recepción del hotel que verifica identidades y reparte llaves; Spring es el sistema de cerraduras que decide qué abre cada llave.

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:groups y username en el token
JWT
firmado
Spring

Las cerraduras: decide qué abre cada llave

  • Valida el JWT (firma, emisor, expiración)
  • Traduce cognito:groups a ROLE_*
  • Aplica la autorización por rol (Capa 1)
  • Te entrega el token para tu lógica de propiedad (Capa 2)
Y del mismo token, cada capa lee un claim distinto:

Claim del JWT

cognito:groups

Responde

¿Qué puedes hacer? (rol)

Quién lo lee

SecurityConfig

Eje

Jerarquía

Claim del JWT

username / sub

Responde

¿Quién eres? (dueño)

Quién lo lee

Controller / Service

Eje

Segmentación

Por qué esto es tan poderoso

Si mueves a un usuario de grupo en la consola de AWS y pide un token nuevo, su permiso cambia sin recompilar, sin reiniciar, sin tocar la base de datos. El JWT es una foto del momento en que se emitió: por eso los tokens expiran rápido, y por eso el token viejo sigue reflejando el rol antiguo hasta que pides uno nuevo.

Resumen para llevar

Pregunta

¿Autenticación o autorización?

Respuesta

AuthN = quién eres (401). AuthZ = qué puedes (403).

Pregunta

¿Las dos formas de autorizar?

Respuesta

Por rol (jerarquía) y por propiedad (segmentación).

Pregunta

¿Dónde viven los roles?

Respuesta

En Cognito (grupos), no en tu base de datos.

Pregunta

¿Cómo llegan al backend?

Respuesta

En cognito:groups del access_token.

Pregunta

¿Quién los traduce a roles Spring?

Respuesta

El JwtAuthenticationConverter → prefijo ROLE_.

Pregunta

¿El ADMIN puede todo?

Respuesta

No. Es otro rol con otro llavero. Cortan en dos sentidos.

Pregunta

¿De dónde sale el dueño de una fila?

Respuesta

Del claim username del JWT, nunca del body.

Pregunta

¿Los dos 403 son lo mismo?

Respuesta

No. Uno es por rol (Spring); el otro por propiedad (tu service).

Pregunta

¿Cuántas tablas agregan los roles?

Respuesta

Cero. Ni users, ni roles, ni user_roles.

En una frase

La jerarquía (roles) decide qué clase de acción puedes hacer; la segmentación (propiedad) decide sobre cuáles datos. Las dos se apoyan en el mismo JWT, pero leen claims distintos y se aplican en capas distintas.
Si quieres ver la parte práctica paso a paso (configurar el Resource Server, validar el JWT de Cognito y proteger endpoints con anotaciones), el post sobre validación de JWT de AWS Cognito en Spring Boot arma el andamiaje completo sobre el que se apoyan estos dos ejes.

Posts que podrian interesarte