Saltar al contenido principal

Protege tu API de Quarkus con control de acceso basado en roles (RBAC) y validación de JWT

Esta guía te ayudará a implementar autorización para asegurar tus APIs de Quarkus utilizando el control de acceso basado en roles (RBAC) y JSON Web Tokens (JWTs) emitidos por Logto.

Antes de comenzar

Tus aplicaciones cliente necesitan obtener tokens de acceso (Access tokens) de Logto. Si aún no has configurado la integración del cliente, consulta nuestras Guías rápidas para React, Vue, Angular u otros frameworks de cliente, o revisa nuestra Guía de máquina a máquina para el acceso de servidor a servidor.

Esta guía se centra en la validación del lado del servidor de esos tokens en tu aplicación Quarkus.

Una figura que muestra el enfoque de esta guía

Lo que aprenderás

  • Validación de JWT: Aprende a validar tokens de acceso (Access tokens) y extraer información de autenticación (Authentication)
  • Implementación de middleware: Crea middleware reutilizable para la protección de API
  • Modelos de permisos: Comprende e implementa diferentes patrones de autorización (Authorization):
    • Recursos de API globales para endpoints a nivel de aplicación
    • Permisos de organización para el control de funciones específicas de inquilinos
    • Recursos de API a nivel de organización para el acceso a datos multi-inquilino
  • Integración de RBAC: Aplica permisos y alcances (Scopes) basados en roles (Roles) en tus endpoints de API

Requisitos previos

  • Última versión estable de Java instalada
  • Conocimientos básicos de Quarkus y desarrollo de API web
  • Una aplicación Logto configurada (consulta las Guías rápidas si es necesario)

Resumen de modelos de permisos

Antes de implementar la protección, elige el modelo de permisos que se adapte a la arquitectura de tu aplicación. Esto se alinea con los tres principales escenarios de autorización de Logto:

RBAC de recursos de API globales
  • Caso de uso: Proteger recursos de API compartidos en toda tu aplicación (no específicos de una organización)
  • Tipo de token: Token de acceso (Access token) con audiencia global
  • Ejemplos: APIs públicas, servicios principales del producto, endpoints de administración
  • Ideal para: Productos SaaS con APIs utilizadas por todos los clientes, microservicios sin aislamiento de inquilinos
  • Más información: Proteger recursos de API globales

💡 Elige tu modelo antes de continuar: la implementación hará referencia a tu enfoque elegido a lo largo de esta guía.

Pasos rápidos de preparación

Configura recursos y permisos de Logto

  1. Crea un recurso de API: Ve a Consola → Recursos de API y registra tu API (por ejemplo, https://api.yourapp.com)
  2. Define permisos: Añade alcances como read:products, write:orders – consulta Definir recursos de API con permisos
  3. Crea roles globales: Ve a Consola → Roles y crea roles que incluyan los permisos de tu API – consulta Configurar roles globales
  4. Asigna roles: Asigna roles a usuarios o aplicaciones M2M que necesiten acceso a la API
¿Nuevo en RBAC?:

Comienza con nuestra Guía de control de acceso basado en roles (RBAC) para instrucciones paso a paso.

Actualiza tu aplicación cliente

Solicita los alcances apropiados en tu cliente:

El proceso normalmente implica actualizar la configuración de tu cliente para incluir uno o más de los siguientes:

  • Parámetro scope en los flujos OAuth
  • Parámetro resource para acceso a recursos de API
  • organization_id para el contexto de organización
Antes de programar:

Asegúrate de que el usuario o la aplicación M2M que estás probando tenga asignados los roles o roles de organización adecuados que incluyan los permisos necesarios para tu API.

Inicializa tu proyecto de API

Para inicializar un nuevo proyecto Quarkus, puedes usar la CLI de Quarkus o Maven:

Usando la CLI de Quarkus:

quarkus create app com.example:your-api-name \
--extension='resteasy-reactive,smallrye-jwt'
cd your-api-name

O usando Maven:

mvn io.quarkus.platform:quarkus-maven-plugin:3.6.0:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=your-api-name \
-Dextensions="resteasy-reactive,smallrye-jwt"
cd your-api-name

Esto creará un proyecto Quarkus básico con las dependencias necesarias:

pom.xml
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>your-api-name</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<compiler-plugin.version>3.11.0</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.6.0</quarkus.platform.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

Crea un recurso básico:

src/main/java/com/example/ExampleResource.java
package com.example;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/hello")
public class ExampleResource {

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello from Quarkus REST";
}
}
nota:

Consulta la documentación de Quarkus para más detalles sobre cómo configurar recursos, servicios y otras funcionalidades.

Inicializa constantes y utilidades

Define las constantes y utilidades necesarias en tu código para manejar la extracción y validación de tokens. Una solicitud válida debe incluir un encabezado Authorization en la forma Bearer <token de acceso (access token)>.

AuthorizationException.java
public class AuthorizationException extends RuntimeException {
private final int statusCode;

public AuthorizationException(String message) {
this(message, 403); // Por defecto 403 Prohibido
}

public AuthorizationException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}

public int getStatusCode() {
return statusCode;
}
}

Recupera información sobre tu tenant de Logto

Necesitarás los siguientes valores para validar los tokens emitidos por Logto:

  • URI de JSON Web Key Set (JWKS): La URL a las claves públicas de Logto, utilizada para verificar las firmas de JWT.
  • Emisor (Issuer): El valor esperado del emisor (la URL OIDC de Logto).

Primero, encuentra el endpoint de tu tenant de Logto. Puedes encontrarlo en varios lugares:

  • En la Consola de Logto, en ConfiguraciónDominios.
  • En cualquier configuración de aplicación que hayas configurado en Logto, ConfiguraciónEndpoints y Credenciales.

Obtener desde el endpoint de descubrimiento de OpenID Connect

Estos valores pueden obtenerse desde el endpoint de descubrimiento de OpenID Connect de Logto:

https://<your-logto-endpoint>/oidc/.well-known/openid-configuration

Aquí tienes un ejemplo de respuesta (otros campos omitidos por brevedad):

{
"jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
"issuer": "https://your-tenant.logto.app/oidc"
}

Dado que Logto no permite personalizar la URI de JWKS ni el emisor, puedes codificar estos valores directamente en tu código. Sin embargo, esto no se recomienda para aplicaciones en producción, ya que puede aumentar la carga de mantenimiento si alguna configuración cambia en el futuro.

  • URI de JWKS: https://<your-logto-endpoint>/oidc/jwks
  • Emisor: https://<your-logto-endpoint>/oidc

Valida el token y los permisos

Después de extraer el token y obtener la configuración OIDC, valida lo siguiente:

  • Firma: El JWT debe ser válido y estar firmado por Logto (a través de JWKS).
  • Emisor (Issuer): Debe coincidir con el emisor de tu tenant de Logto.
  • Audiencia (Audience): Debe coincidir con el indicador de recurso de la API registrado en Logto, o el contexto de la organización si corresponde.
  • Expiración: El token no debe estar expirado.
  • Permisos (Alcances / scopes): El token debe incluir los alcances requeridos para tu API / acción. Los alcances son cadenas separadas por espacios en el reclamo scope.
  • Contexto de organización: Si proteges recursos de API a nivel de organización, valida el reclamo organization_id.

Consulta JSON Web Token para aprender más sobre la estructura y los reclamos de JWT.

Qué verificar para cada modelo de permisos

  • Reclamo de audiencia (aud): Indicador de recurso de API
  • Reclamo de organización (organization_id): No presente
  • Alcances (permisos) a verificar (scope): Permisos de recurso de API

Para los permisos de organización que no son de API, el contexto de la organización está representado por el reclamo aud (por ejemplo, urn:logto:organization:abc123). El reclamo organization_id solo está presente para tokens de recursos de API a nivel de organización.

tip:

Valida siempre tanto los permisos (alcances) como el contexto (audiencia, organización) para APIs multi-tenant seguras.

Añade la lógica de validación

Usamos diferentes bibliotecas JWT dependiendo del framework. Instala las dependencias requeridas:

Agrega a tu pom.xml:

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
application.properties
# Configuración de JWT
mp.jwt.verify.publickey.location=${JWKS_URI:https://your-tenant.logto.app/oidc/jwks}
mp.jwt.verify.issuer=${JWT_ISSUER:https://your-tenant.logto.app/oidc}
JwtVerificationFilter.java
import org.eclipse.microprofile.jwt.JsonWebToken;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import java.util.ArrayList;
import java.util.List;

@Provider
@ApplicationScoped
public class JwtVerificationFilter implements ContainerRequestFilter {

@Inject
JsonWebToken jwt;

@Override
public void filter(ContainerRequestContext requestContext) {
if (requestContext.getUriInfo().getPath().startsWith("/api/protected")) {
try {
verifyPayload(jwt);
requestContext.setProperty("auth", jwt);
} catch (AuthorizationException e) {
requestContext.abortWith(
Response.status(e.getStatusCode())
.entity("{\"error\": \"" + e.getMessage() + "\"}")
.build()
);
} catch (Exception e) {
requestContext.abortWith(
Response.status(401)
.entity("{\"error\": \"Token inválido\"}")
.build()
);
}
}
}

private void verifyPayload(JsonWebToken jwt) {
// La validación del emisor (Issuer) se maneja automáticamente por la extensión JWT de Quarkus
// Implementa aquí tu lógica de verificación adicional basada en el modelo de permisos
// Utiliza los métodos auxiliares de abajo para la extracción de reclamos (claims)
}

// Métodos auxiliares para Quarkus JWT
private List<String> extractAudiences(JsonWebToken jwt) {
return new ArrayList<>(jwt.getAudience());
}

private String extractScopes(JsonWebToken jwt) {
return jwt.getClaim("scope");
}

private String extractOrganizationId(JsonWebToken jwt) {
return jwt.getClaim("organization_id");
}
}

De acuerdo con tu modelo de permisos, implementa la lógica de verificación adecuada:

// Verifica que el reclamo de audiencia coincida con tu indicador de recurso de API
List<String> audiences = extractAudiences(token); // Extracción específica del framework
if (!audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Audiencia no válida");
}

// Verifica los alcances requeridos para recursos de API globales
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Reemplaza con tus alcances requeridos reales
String scopes = extractScopes(token); // Extracción específica del framework
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();

if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Alcance insuficiente");
}

Los métodos auxiliares para extraer reclamos son específicos del framework. Consulta los detalles de implementación en los archivos de validación específicos del framework mencionados arriba.

Aplica el middleware a tu API

Ahora, aplica el middleware a tus rutas de API protegidas.

ProtectedResource.java
import org.eclipse.microprofile.jwt.JsonWebToken;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.container.ContainerRequestContext;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

@Path("/api")
public class ProtectedResource {

@Inject
JsonWebToken jwt;

@GET
@Path("/protected")
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Object> protectedEndpoint(@Context ContainerRequestContext requestContext) {
// Accede al JWT directamente desde la inyección o el contexto
JsonWebToken token = (JsonWebToken) requestContext.getProperty("auth");
if (token == null) {
token = jwt; // Alternativa al JWT inyectado
}

String scopes = token.getClaim("scope");
List<String> scopeList = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();

return Map.of(
"sub", token.getSubject(),
"client_id", token.<String>getClaim("client_id"),
"organization_id", token.<String>getClaim("organization_id"),
"scopes", scopeList,
"audience", token.getAudience()
);
}
}

Prueba tu API protegida

Obtener tokens de acceso (Access tokens)

Desde tu aplicación cliente: Si has configurado una integración de cliente, tu aplicación puede obtener tokens automáticamente. Extrae el token de acceso y úsalo en las solicitudes a la API.

Para pruebas con curl / Postman:

  1. Tokens de usuario: Usa las herramientas de desarrollador de tu aplicación cliente para copiar el token de acceso desde localStorage o la pestaña de red.

  2. Tokens máquina a máquina: Utiliza el flujo de credenciales de cliente. Aquí tienes un ejemplo no normativo usando curl:

    curl -X POST https://your-tenant.logto.app/oidc/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=client_credentials" \
    -d "client_id=your-m2m-client-id" \
    -d "client_secret=your-m2m-client-secret" \
    -d "resource=https://your-api-resource-indicator" \
    -d "scope=api:read api:write"

    Puede que necesites ajustar los parámetros resource y scope según tu recurso de API y permisos; también puede ser necesario un parámetro organization_id si tu API está asociada a una organización.

tip:

¿Necesitas inspeccionar el contenido del token? Usa nuestro decodificador de JWT para decodificar y verificar tus JWTs.

Probar endpoints protegidos

Solicitud con token válido
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:3000/api/protected

Respuesta esperada:

{
"auth": {
"sub": "user123",
"clientId": "app456",
"organizationId": "org789",
"scopes": ["api:read", "api:write"],
"audience": ["https://your-api-resource-indicator"]
}
}
Token ausente
curl http://localhost:3000/api/protected

Respuesta esperada (401):

{
"error": "Authorization header is missing"
}
Token inválido
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3000/api/protected

Respuesta esperada (401):

{
"error": "Invalid token"
}

Pruebas específicas del modelo de permisos

Escenarios de prueba para APIs protegidas con alcances globales:

  • Alcances válidos: Prueba con tokens que incluyan los alcances de API requeridos (por ejemplo, api:read, api:write)
  • Alcances ausentes: Espera 403 Prohibido cuando el token no tenga los alcances requeridos
  • Audiencia incorrecta: Espera 403 Prohibido cuando la audiencia no coincida con el recurso de API
# Token sin los alcances requeridos - espera 403
curl -H "Authorization: Bearer token-without-required-scopes" \
http://localhost:3000/api/protected

Lecturas adicionales

RBAC en la práctica: Implementando autorización segura para tu aplicación

Crea una aplicación SaaS multi-inquilino: Una guía completa desde el diseño hasta la implementación