Cómo validar tokens de acceso en tu servicio API o backend
Validar los tokens de acceso es una parte crítica para hacer cumplir el control de acceso basado en roles (RBAC) en Logto. Esta guía te guía a través de la verificación de JWTs emitidos por Logto en tu backend / API, comprobando la firma, emisor, audiencia, expiración, permisos (alcances) y contexto de organización.
Antes de comenzar
- Esta guía asume que estás familiarizado con los conceptos de RBAC de Logto.
- Si estás protegiendo recursos de API, esta guía asume que ya has seguido la guía Proteger recursos de API globales.
- Si estás protegiendo funciones o flujos dentro de la aplicación (permisos no-API), esta guía asume que ya has seguido la guía Proteger permisos de organización (no-API).
- Si estás protegiendo recursos de API a nivel de organización, esta guía asume que ya has seguido la guía Proteger recursos de API a nivel de organización.
Paso 1: Inicializa constantes y utilidades
Define las constantes y utilidades necesarias en tu código para manejar la extracción y validación del token. Una solicitud válida debe incluir un encabezado Authorization
en la forma Bearer <access_token>
.
- Node.js
- Python
- Go
- Java
- .NET
- PHP
- Ruby
- Rust
import { IncomingHttpHeaders } from 'http';
const JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks';
const ISSUER = 'https://your-tenant.logto.app/oidc';
export class AuthInfo {
constructor(
public sub: string,
public clientId?: string,
public organizationId?: string,
public scopes: string[] = [],
public audience: string[] = []
) {}
}
export class AuthorizationError extends Error {
name = 'AuthorizationError';
constructor(
message: string,
public status = 403
) {
super(message);
}
}
export function extractBearerTokenFromHeaders({ authorization }: IncomingHttpHeaders): string {
const bearerPrefix = 'Bearer ';
if (!authorization) {
throw new AuthorizationError('Authorization header is missing', 401);
}
if (!authorization.startsWith(bearerPrefix)) {
throw new AuthorizationError(`Authorization header must start with "${bearerPrefix}"`, 401);
}
return authorization.slice(bearerPrefix.length);
}
JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks'
ISSUER = 'https://your-tenant.logto.app/oidc'
class AuthInfo:
def __init__(self, sub: str, client_id: str = None, organization_id: str = None,
scopes: list = None, audience: list = None):
self.sub = sub
self.client_id = client_id
self.organization_id = organization_id
self.scopes = scopes or []
self.audience = audience or []
def to_dict(self):
return {
'sub': self.sub,
'client_id': self.client_id,
'organization_id': self.organization_id,
'scopes': self.scopes,
'audience': self.audience
}
class AuthorizationError(Exception):
def __init__(self, message: str, status: int = 403):
self.message = message
self.status = status
super().__init__(self.message)
def extract_bearer_token_from_headers(headers: dict) -> str:
"""
Extrae el token bearer de los encabezados HTTP.
Nota: FastAPI y Django REST Framework tienen extracción de tokens incorporada,
por lo que esta función es principalmente para Flask y otros frameworks.
"""
authorization = headers.get('authorization') or headers.get('Authorization')
if not authorization:
raise AuthorizationError('El encabezado de autorización falta', 401)
if not authorization.startswith('Bearer '):
raise AuthorizationError('El encabezado de autorización debe comenzar con "Bearer "', 401)
return authorization[7:] # Elimina el prefijo 'Bearer '
package main
import (
"fmt"
"net/http"
"strings"
)
const (
JWKS_URI = "https://your-tenant.logto.app/oidc/jwks"
ISSUER = "https://your-tenant.logto.app/oidc"
)
type AuthorizationError struct {
Message string
Status int
}
func (e *AuthorizationError) Error() string {
return e.Message
}
func NewAuthorizationError(message string, status ...int) *AuthorizationError {
statusCode := http.StatusForbidden // Por defecto 403 Prohibido
if len(status) > 0 {
statusCode = status[0]
}
return &AuthorizationError{
Message: message,
Status: statusCode,
}
}
func extractBearerTokenFromHeaders(r *http.Request) (string, error) {
const bearerPrefix = "Bearer "
authorization := r.Header.Get("Authorization")
if authorization == "" {
return "", NewAuthorizationError("El encabezado Authorization (Authorization header) falta", http.StatusUnauthorized)
}
if !strings.HasPrefix(authorization, bearerPrefix) {
return "", NewAuthorizationError(fmt.Sprintf("El encabezado Authorization (Authorization header) debe comenzar con \"%s\"", bearerPrefix), http.StatusUnauthorized)
}
return strings.TrimPrefix(authorization, bearerPrefix), nil
}
public final class AuthConstants {
public static final String JWKS_URI = "https://your-tenant.logto.app/oidc/jwks";
public static final String ISSUER = "https://your-tenant.logto.app/oidc";
private AuthConstants() {
// Prevenir la instanciación (Prevent instantiation)
}
}
public class AuthorizationException extends RuntimeException {
private final int statusCode;
public AuthorizationException(String message) {
this(message, 403); // Por defecto 403 Prohibido (Default to 403 Forbidden)
}
public AuthorizationException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}
public int getStatusCode() {
return statusCode;
}
}
namespace YourApiNamespace
{
public static class AuthConstants
{
public const string Issuer = "https://your-tenant.logto.app/oidc";
}
}
namespace YourApiNamespace.Exceptions
{
public class AuthorizationException : Exception
{
public int StatusCode { get; }
public AuthorizationException(string message, int statusCode = 403) : base(message)
{
StatusCode = statusCode;
}
}
}
<?php
class AuthConstants
{
public const JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks';
public const ISSUER = 'https://your-tenant.logto.app/oidc';
}
<?php
class AuthInfo
{
public function __construct(
public readonly string $sub,
public readonly ?string $clientId = null,
public readonly ?string $organizationId = null,
public readonly array $scopes = [],
public readonly array $audience = []
) {}
public function toArray(): array
{
return [
'sub' => $this->sub,
'client_id' => $this->clientId,
'organization_id' => $this->organizationId,
'scopes' => $this->scopes,
'audience' => $this->audience,
];
}
}
<?php
class AuthorizationException extends Exception
{
public function __construct(
string $message,
public readonly int $statusCode = 403
) {
parent::__construct($message);
}
}
<?php
trait AuthHelpers
{
protected function extractBearerToken(array $headers): string
{
$authorization = $headers['authorization'][0] ?? $headers['Authorization'][0] ?? null;
if (!$authorization) {
throw new AuthorizationException('El encabezado de autorización (Authorization header) falta', 401);
}
if (!str_starts_with($authorization, 'Bearer ')) {
throw new AuthorizationException('El encabezado de autorización (Authorization header) debe comenzar con "Bearer "', 401);
}
return substr($authorization, 7); // Elimina el prefijo 'Bearer '
}
}
module AuthConstants
JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks'
ISSUER = 'https://your-tenant.logto.app/oidc'
end
class AuthInfo
attr_accessor :sub, :client_id, :organization_id, :scopes, :audience
def initialize(sub, client_id = nil, organization_id = nil, scopes = [], audience = [])
@sub = sub
@client_id = client_id
@organization_id = organization_id
@scopes = scopes
@audience = audience
end
def to_h
{
sub: @sub,
client_id: @client_id,
organization_id: @organization_id,
scopes: @scopes,
audience: @audience
}
end
end
class AuthorizationError < StandardError
attr_reader :status
def initialize(message, status = 403)
super(message)
@status = status
end
end
module AuthHelpers
def extract_bearer_token(request)
authorization = request.headers['Authorization']
raise AuthorizationError.new('Authorization header is missing', 401) unless authorization
raise AuthorizationError.new('Authorization header must start with "Bearer "', 401) unless authorization.start_with?('Bearer ')
authorization[7..-1] # Remove 'Bearer ' prefix
end
end
use serde::{Deserialize, Serialize};
use std::fmt;
pub const JWKS_URI: &str = "https://your-tenant.logto.app/oidc/jwks";
pub const ISSUER: &str = "https://your-tenant.logto.app/oidc";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthInfo {
pub sub: String,
pub client_id: Option<String>,
pub organization_id: Option<String>,
pub scopes: Vec<String>,
pub audience: Vec<String>,
}
impl AuthInfo {
pub fn new(
sub: String,
client_id: Option<String>,
organization_id: Option<String>,
scopes: Vec<String>,
audience: Vec<String>,
) -> Self {
Self {
sub,
client_id,
organization_id,
scopes,
audience,
}
}
}
#[derive(Debug)]
pub struct AuthorizationError {
pub message: String,
pub status_code: u16,
}
impl AuthorizationError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
status_code: 403,
}
}
pub fn with_status(message: impl Into<String>, status_code: u16) -> Self {
Self {
message: message.into(),
status_code,
}
}
}
impl fmt::Display for AuthorizationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for AuthorizationError {}
pub fn extract_bearer_token(authorization: Option<&str>) -> Result<&str, AuthorizationError> {
let auth_header = authorization.ok_or_else(|| {
AuthorizationError::with_status("Falta el encabezado de autorización (Authorization header is missing)", 401)
})?;
if !auth_header.starts_with("Bearer ") {
return Err(AuthorizationError::with_status(
"El encabezado de autorización debe comenzar con \"Bearer \" (Authorization header must start with \"Bearer \")",
401,
));
}
Ok(&auth_header[7..]) // Elimina el prefijo 'Bearer ' (Remove 'Bearer ' prefix)
}
Paso 2: Obtén 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 de las claves públicas de Logto, utilizada para verificar las firmas de los JWT.
- Emisor (Issuer): El valor esperado de 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ón → Dominios.
- En cualquier configuración de aplicación que hayas configurado en Logto, Configuración → Endpoints 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"
}
Codificar directamente en tu código (no recomendado)
Dado que Logto no permite personalizar el 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.
- JWKS URI:
https://<your-logto-endpoint>/oidc/jwks
- Emisor:
https://<your-logto-endpoint>/oidc
Paso 3: 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 organización si aplica.
- 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 los JWT.
Qué comprobar según el modelo de permisos
Los reclamos y reglas de validación difieren según el modelo de permisos:
Modelo de permisos | Reclamo de audiencia (aud ) | Reclamo de organización (organization_id ) | Alcances (permisos) a comprobar (scope ) |
---|---|---|---|
Recursos de API globales | Indicador de recurso de API | No presente | Permisos de recurso de API |
Permisos de organización (no-API) | urn:logto:organization:<id> (el contexto de organización está en aud ) | No presente | Permisos de organización |
Recursos de API a nivel de organización | Indicador de recurso de API | ID de organización (debe coincidir con la solicitud) | Permisos de recurso de API |
Para los permisos de organización no-API, el contexto de 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.
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
- Node.js
- Python
- Go
- Java
- .NET
- PHP
- Ruby
- Rust
Usamos jose en este ejemplo para validar el JWT. Instálalo si aún no lo has hecho:
npm install jose
O utiliza tu gestor de paquetes preferido (por ejemplo, pnpm
o yarn
).
Primero, añade estas utilidades compartidas para manejar la validación de JWT:
import { createRemoteJWKSet, jwtVerify, JWTPayload } from 'jose';
import { AuthInfo, AuthorizationError } from './auth-middleware.js';
const jwks = createRemoteJWKSet(new URL(JWKS_URI));
export async function validateJwtToken(token: string): Promise<JWTPayload> {
const { payload } = await jwtVerify(token, jwks, {
issuer: ISSUER,
});
verifyPayload(payload);
return payload;
}
export function createAuthInfo(payload: JWTPayload): AuthInfo {
const scopes = (payload.scope as string)?.split(' ') ?? [];
const audience = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
return new AuthInfo(
payload.sub!,
payload.client_id as string,
payload.organization_id as string,
scopes,
audience
);
}
function verifyPayload(payload: JWTPayload): void {
// Implementa aquí tu lógica de verificación basada en el modelo de permisos
// Esto se mostrará en la sección de modelos de permisos más abajo
}
Luego, implementa el middleware para verificar el token de acceso (Access token):
- Express.js
- Koa.js
- Fastify
- Hapi.js
- NestJS
import { Request, Response, NextFunction } from 'express';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
// Extiende la interfaz Request de Express para incluir auth
declare global {
namespace Express {
interface Request {
auth?: AuthInfo;
}
}
}
export async function verifyAccessToken(req: Request, res: Response, next: NextFunction) {
try {
const token = extractBearerTokenFromHeaders(req.headers);
const payload = await validateJwtToken(token);
// Almacena la información de autenticación en la solicitud para uso general
req.auth = createAuthInfo(payload);
next();
} catch (err: any) {
return res.status(err.status ?? 401).json({ error: err.message });
}
}
import { Context, Next } from 'koa';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
export async function koaVerifyAccessToken(ctx: Context, next: Next) {
try {
const token = extractBearerTokenFromHeaders(ctx.request.headers);
const payload = await validateJwtToken(token);
// Almacena la información de autenticación en state para uso general
ctx.state.auth = createAuthInfo(payload);
await next();
} catch (err: any) {
ctx.status = err.status ?? 401;
ctx.body = { error: err.message };
}
}
import { FastifyRequest, FastifyReply } from 'fastify';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
// Extiende la interfaz Request de Fastify para incluir auth
declare module 'fastify' {
interface FastifyRequest {
auth?: AuthInfo;
}
}
export async function fastifyVerifyAccessToken(request: FastifyRequest, reply: FastifyReply) {
try {
const token = extractBearerTokenFromHeaders(request.headers);
const payload = await validateJwtToken(token);
// Almacena la información de autenticación en la solicitud para uso general
request.auth = createAuthInfo(payload);
} catch (err: any) {
reply.code(err.status ?? 401).send({ error: err.message });
}
}
import { Request, ResponseToolkit } from '@hapi/hapi';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
export async function hapiVerifyAccessToken(request: Request, h: ResponseToolkit) {
try {
const token = extractBearerTokenFromHeaders(request.headers);
const payload = await validateJwtToken(token);
// Almacena la información de autenticación en request.app para uso general
request.app.auth = createAuthInfo(payload);
return h.continue;
} catch (err: any) {
return h
.response({ error: err.message })
.code(err.status ?? 401)
.takeover();
}
}
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
ForbiddenException,
} from '@nestjs/common';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
@Injectable()
export class AccessTokenGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const token = extractBearerTokenFromHeaders(req.headers);
const payload = await validateJwtToken(token);
// Almacena la información de autenticación en la solicitud para uso general
req.auth = createAuthInfo(payload);
return true;
} catch (err: any) {
if (err.status === 401) throw new UnauthorizedException(err.message);
throw new ForbiddenException(err.message);
}
}
}
De acuerdo con tu modelo de permisos, implementa la lógica de verificación apropiada en jwt-validator.ts
:
- Recursos de API globales
- Permisos de organización (no API)
- Recursos de API a nivel de organización
function verifyPayload(payload: JWTPayload): void {
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
const audiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
if (!audiences.includes('https://your-api-resource-indicator')) {
throw new AuthorizationError('Audiencia inválida');
}
// Verifica los alcances requeridos para recursos de API globales
const requiredScopes = ['api:read', 'api:write']; // Reemplaza con tus alcances requeridos
const scopes = (payload.scope as string)?.split(' ') ?? [];
if (!requiredScopes.every((scope) => scopes.includes(scope))) {
throw new AuthorizationError('Alcance insuficiente');
}
}
function verifyPayload(payload: JWTPayload): void {
// Verifica que el claim de audiencia coincida con el formato de organización
const audiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
const hasOrgAudience = audiences.some((aud) => aud.startsWith('urn:logto:organization:'));
if (!hasOrgAudience) {
throw new AuthorizationError('Audiencia inválida para permisos de organización');
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
const expectedOrgId = 'your-organization-id'; // Extrae del contexto de la solicitud
const expectedAud = `urn:logto:organization:${expectedOrgId}`;
if (!audiences.includes(expectedAud)) {
throw new AuthorizationError('ID de organización no coincide');
}
// Verifica los alcances requeridos de la organización
const requiredScopes = ['invite:users', 'manage:settings']; // Reemplaza con tus alcances requeridos
const scopes = (payload.scope as string)?.split(' ') ?? [];
if (!requiredScopes.every((scope) => scopes.includes(scope))) {
throw new AuthorizationError('Alcance de organización insuficiente');
}
}
function verifyPayload(payload: JWTPayload): void {
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
const audiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
if (!audiences.includes('https://your-api-resource-indicator')) {
throw new AuthorizationError('Audiencia inválida para recursos de API a nivel de organización');
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
const expectedOrgId = 'your-organization-id'; // Extrae del contexto de la solicitud
const orgId = payload.organization_id as string;
if (expectedOrgId !== orgId) {
throw new AuthorizationError('ID de organización no coincide');
}
// Verifica los alcances requeridos para recursos de API a nivel de organización
const requiredScopes = ['api:read', 'api:write']; // Reemplaza con tus alcances requeridos
const scopes = (payload.scope as string)?.split(' ') ?? [];
if (!requiredScopes.every((scope) => scopes.includes(scope))) {
throw new AuthorizationError(
'Alcances insuficientes para recursos de API a nivel de organización'
);
}
}
Usamos PyJWT para validar JWTs. Instálalo si aún no lo has hecho:
pip install pyjwt[crypto]
Primero, añade estas utilidades compartidas para manejar la validación de JWT:
import jwt
from jwt import PyJWKClient
from typing import Dict, Any
from auth_middleware import AuthInfo, AuthorizationError, JWKS_URI, ISSUER
jwks_client = PyJWKClient(JWKS_URI)
def validate_jwt_token(token: str) -> Dict[str, Any]:
"""Validar el token JWT y devolver el payload"""
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
issuer=ISSUER,
options={'verify_aud': False} # Verificaremos la audiencia manualmente
)
verify_payload(payload)
return payload
except jwt.InvalidTokenError as e:
raise AuthorizationError(f'Token inválido: {str(e)}', 401)
except Exception as e:
raise AuthorizationError(f'La validación del token falló: {str(e)}', 401)
def create_auth_info(payload: Dict[str, Any]) -> AuthInfo:
"""Crear AuthInfo a partir del payload del JWT"""
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
audience = payload.get('aud', [])
if isinstance(audience, str):
audience = [audience]
return AuthInfo(
sub=payload.get('sub'),
client_id=payload.get('client_id'),
organization_id=payload.get('organization_id'),
scopes=scopes,
audience=audience
)
def verify_payload(payload: Dict[str, Any]) -> None:
"""Verificar el payload según el modelo de permisos"""
# Implementa aquí tu lógica de verificación basada en el modelo de permisos
# Esto se mostrará en la sección de modelos de permisos a continuación
pass
Luego, implementa el middleware para verificar el token de acceso (access token):
- FastAPI
- Flask
- Django
- Django REST Framework
from fastapi import HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jwt_validator import validate_jwt_token, create_auth_info
security = HTTPBearer()
async def verify_access_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> AuthInfo:
try:
token = credentials.credentials
payload = validate_jwt_token(token)
return create_auth_info(payload)
except AuthorizationError as e:
raise HTTPException(status_code=e.status, detail=str(e))
from functools import wraps
from flask import request, jsonify, g
from jwt_validator import validate_jwt_token, create_auth_info
def verify_access_token(f):
@wraps(f)
def decorated_function(*args, **kwargs):
try:
token = extract_bearer_token_from_headers(dict(request.headers))
payload = validate_jwt_token(token)
# Almacenar la información de autenticación en el objeto g de Flask para uso general
g.auth = create_auth_info(payload)
return f(*args, **kwargs)
except AuthorizationError as e:
return jsonify({'error': str(e)}), e.status
return decorated_function
from django.http import JsonResponse
from jwt_validator import validate_jwt_token, create_auth_info
def require_access_token(view_func):
def wrapper(request, *args, **kwargs):
try:
headers = {key.replace('HTTP_', '').replace('_', '-').lower(): value
for key, value in request.META.items() if key.startswith('HTTP_')}
token = extract_bearer_token_from_headers(headers)
payload = validate_jwt_token(token)
# Adjuntar la información de autenticación a la request para uso general
request.auth = create_auth_info(payload)
return view_func(request, *args, **kwargs)
except AuthorizationError as e:
return JsonResponse({'error': str(e)}, status=e.status)
return wrapper
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions
from jwt_validator import validate_jwt_token, create_auth_info
class AccessTokenAuthentication(TokenAuthentication):
keyword = 'Bearer' # Usar 'Bearer' en lugar de 'Token'
def authenticate_credentials(self, key):
"""
Autenticar el token validándolo como un JWT.
"""
try:
payload = validate_jwt_token(key)
auth_info = create_auth_info(payload)
# Crear un objeto tipo usuario que contenga la información de autenticación para uso general
user = type('User', (), {
'auth': auth_info,
'is_authenticated': True,
'is_anonymous': False,
'is_active': True,
})()
return (user, key)
except AuthorizationError as e:
if e.status == 401:
raise exceptions.AuthenticationFailed(str(e))
else: # 403
raise exceptions.PermissionDenied(str(e))
De acuerdo con tu modelo de permisos, implementa la lógica de verificación apropiada en jwt_validator.py
:
- Recursos de API globales
- Permisos de organización (no API)
- Recursos de API a nivel de organización
def verify_payload(payload: Dict[str, Any]) -> None:
"""Verificar el payload para recursos de API globales"""
# Comprobar que el claim de audiencia coincida con tu indicador de recurso de API
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('Audiencia inválida')
# Comprobar los alcances requeridos para recursos de API globales
required_scopes = ['api:read', 'api:write'] # Reemplaza con tus alcances requeridos
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Alcance insuficiente')
def verify_payload(payload: Dict[str, Any]) -> None:
"""Verificar el payload para permisos de organización"""
# Comprobar que el claim de audiencia coincida con el formato de organización
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
has_org_audience = any(aud.startswith('urn:logto:organization:') for aud in audiences)
if not has_org_audience:
raise AuthorizationError('Audiencia inválida para permisos de organización')
# Comprobar que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la request)
expected_org_id = 'your-organization-id' # Extraer del contexto de la request
expected_aud = f'urn:logto:organization:{expected_org_id}'
if expected_aud not in audiences:
raise AuthorizationError('ID de organización no coincide')
# Comprobar los alcances requeridos de la organización
required_scopes = ['invite:users', 'manage:settings'] # Reemplaza con tus alcances requeridos
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Alcance de organización insuficiente')
def verify_payload(payload: Dict[str, Any]) -> None:
"""Verificar el payload para recursos de API a nivel de organización"""
# Comprobar que el claim de audiencia coincida con tu indicador de recurso de API
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('Audiencia inválida para recursos de API a nivel de organización')
# Comprobar que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la request)
expected_org_id = 'your-organization-id' # Extraer del contexto de la request
org_id = payload.get('organization_id')
if expected_org_id != org_id:
raise AuthorizationError('ID de organización no coincide')
# Comprobar los alcances requeridos para recursos de API a nivel de organización
required_scopes = ['api:read', 'api:write'] # Reemplaza con tus alcances requeridos
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Alcances insuficientes para recursos de API a nivel de organización')
Usamos github.com/lestrrat-go/jwx para validar JWTs. Instálalo si aún no lo has hecho:
go mod init your-project
go get github.com/lestrrat-go/jwx/v3
Primero, añade estos componentes compartidos a tu archivo auth_middleware.go
:
import (
"context"
"strings"
"time"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
)
var jwkSet jwk.Set
func init() {
// Inicializar la caché de JWKS
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var err error
jwkSet, err = jwk.Fetch(ctx, JWKS_URI)
if err != nil {
panic("No se pudo obtener JWKS: " + err.Error())
}
}
// validateJWTToken valida el token JWT y devuelve el token analizado
func validateJWTToken(tokenString string) (jwt.Token, error) {
token, err := jwt.Parse([]byte(tokenString), jwt.WithKeySet(jwkSet))
if err != nil {
return nil, NewAuthorizationError("Token inválido: "+err.Error(), http.StatusUnauthorized)
}
// Verificar emisor
if token.Issuer() != ISSUER {
return nil, NewAuthorizationError("Emisor inválido", http.StatusUnauthorized)
}
if err := verifyPayload(token); err != nil {
return nil, err
}
return token, nil
}
// Funciones auxiliares para extraer datos del token
func getStringClaim(token jwt.Token, key string) string {
if val, ok := token.Get(key); ok {
if str, ok := val.(string); ok {
return str
}
}
return ""
}
func getScopesFromToken(token jwt.Token) []string {
if val, ok := token.Get("scope"); ok {
if scope, ok := val.(string); ok && scope != "" {
return strings.Split(scope, " ")
}
}
return []string{}
}
func getAudienceFromToken(token jwt.Token) []string {
return token.Audience()
}
Luego, implementa el middleware para verificar el token de acceso (Access token):
- Gin
- Echo
- Fiber
- Chi
import "github.com/gin-gonic/gin"
func VerifyAccessToken() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString, err := extractBearerTokenFromHeaders(c.Request)
if err != nil {
authErr := err.(*AuthorizationError)
c.JSON(authErr.Status, gin.H{"error": authErr.Message})
c.Abort()
return
}
token, err := validateJWTToken(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
c.JSON(authErr.Status, gin.H{"error": authErr.Message})
c.Abort()
return
}
// Guardar el token en el contexto para uso general
c.Set("auth", token)
c.Next()
}
}
import "github.com/labstack/echo/v4"
func VerifyAccessToken(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
tokenString, err := extractBearerTokenFromHeaders(c.Request())
if err != nil {
authErr := err.(*AuthorizationError)
return c.JSON(authErr.Status, echo.Map{"error": authErr.Message})
}
token, err := validateJWTToken(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
return c.JSON(authErr.Status, echo.Map{"error": authErr.Message})
}
// Guardar el token en el contexto para uso general
c.Set("auth", token)
return next(c)
}
}
import (
"net/http"
"github.com/gofiber/fiber/v2"
)
func VerifyAccessToken(c *fiber.Ctx) error {
// Convertir la solicitud de fiber a http.Request para compatibilidad
req := &http.Request{
Header: make(http.Header),
}
req.Header.Set("Authorization", c.Get("Authorization"))
tokenString, err := extractBearerTokenFromHeaders(req)
if err != nil {
authErr := err.(*AuthorizationError)
return c.Status(authErr.Status).JSON(fiber.Map{"error": authErr.Message})
}
token, err := validateJWTToken(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
return c.Status(authErr.Status).JSON(fiber.Map{"error": authErr.Message})
}
// Guardar el token en locals para uso general
c.Locals("auth", token)
return c.Next()
}
import (
"context"
"encoding/json"
"net/http"
)
type contextKey string
const AuthContextKey contextKey = "auth"
func VerifyAccessToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString, err := extractBearerTokenFromHeaders(r)
if err != nil {
authErr := err.(*AuthorizationError)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(authErr.Status)
json.NewEncoder(w).Encode(map[string]string{"error": authErr.Message})
return
}
token, err := validateJWTToken(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(authErr.Status)
json.NewEncoder(w).Encode(map[string]string{"error": authErr.Message})
return
}
// Guardar el token en el contexto para uso general
ctx := context.WithValue(r.Context(), AuthContextKey, token)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Según tu modelo de permisos, puede que necesites adoptar una lógica diferente para verifyPayload
:
- Recursos de API globales
- Permisos de organización (no API)
- Recursos de API a nivel de organización
func verifyPayload(token jwt.Token) error {
// Verificar que el claim de audiencia coincida con tu indicador de recurso de API
if !hasAudience(token, "https://your-api-resource-indicator") {
return NewAuthorizationError("Audiencia inválida")
}
// Verificar los alcances requeridos para recursos de API globales
requiredScopes := []string{"api:read", "api:write"} // Reemplaza con tus alcances requeridos
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Alcance insuficiente")
}
return nil
}
func verifyPayload(token jwt.Token) error {
// Verificar que el claim de audiencia coincida con el formato de organización
if !hasOrganizationAudience(token) {
return NewAuthorizationError("Audiencia inválida para permisos de organización")
}
// Verificar que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
expectedOrgID := "your-organization-id" // Extraer del contexto de la solicitud
if !hasMatchingOrganization(token, expectedOrgID) {
return NewAuthorizationError("ID de organización no coincide")
}
// Verificar los alcances requeridos de la organización
requiredScopes := []string{"invite:users", "manage:settings"} // Reemplaza con tus alcances requeridos
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Alcance de organización insuficiente")
}
return nil
}
func verifyPayload(token jwt.Token) error {
// Verificar que el claim de audiencia coincida con tu indicador de recurso de API
if !hasAudience(token, "https://your-api-resource-indicator") {
return NewAuthorizationError("Audiencia inválida para recursos de API a nivel de organización")
}
// Verificar que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
expectedOrgID := "your-organization-id" // Extraer del contexto de la solicitud
if !hasMatchingOrganizationID(token, expectedOrgID) {
return NewAuthorizationError("ID de organización no coincide")
}
// Verificar los alcances requeridos para recursos de API a nivel de organización
requiredScopes := []string{"api:read", "api:write"} // Reemplaza con tus alcances requeridos
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Alcances insuficientes para recursos de API a nivel de organización")
}
return nil
}
Agrega estas funciones auxiliares para la verificación del payload:
// hasAudience verifica si el token tiene la audiencia especificada
func hasAudience(token jwt.Token, expectedAud string) bool {
audiences := token.Audience()
for _, aud := range audiences {
if aud == expectedAud {
return true
}
}
return false
}
// hasOrganizationAudience verifica si el token tiene formato de audiencia de organización
func hasOrganizationAudience(token jwt.Token) bool {
audiences := token.Audience()
for _, aud := range audiences {
if strings.HasPrefix(aud, "urn:logto:organization:") {
return true
}
}
return false
}
// hasRequiredScopes verifica si el token tiene todos los alcances requeridos
func hasRequiredScopes(token jwt.Token, requiredScopes []string) bool {
scopes := getScopesFromToken(token)
for _, required := range requiredScopes {
found := false
for _, scope := range scopes {
if scope == required {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// hasMatchingOrganization verifica si la audiencia del token coincide con la organización esperada
func hasMatchingOrganization(token jwt.Token, expectedOrgID string) bool {
expectedAud := fmt.Sprintf("urn:logto:organization:%s", expectedOrgID)
return hasAudience(token, expectedAud)
}
// hasMatchingOrganizationID verifica si el organization_id del token coincide con el esperado
func hasMatchingOrganizationID(token jwt.Token, expectedOrgID string) bool {
orgID := getStringClaim(token, "organization_id")
return orgID == expectedOrgID
}
Usamos diferentes librerías JWT dependiendo del framework. Instala las dependencias requeridas:
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
Agrega a tu pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
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>
Agrega a tu pom.xml
:
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
</dependency>
Agrega a tu pom.xml
:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-auth-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-client</artifactId>
</dependency>
Luego, implementa el middleware para verificar el token de acceso (Access token):
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/protected/**").authenticated()
.anyRequest().permitAll()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder()))
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(AuthConstants.JWKS_URI)
.build();
}
}
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
public class JwtValidator {
public void verifyPayload(Jwt jwt) {
// Implementa aquí tu lógica de verificación basada en el modelo de permisos
// Esto se mostrará en la sección de modelos de permisos más abajo
}
}
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.ext.Provider;
@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);
// Almacena el JWT en el contexto para acceso en los controladores
requestContext.setProperty("auth", jwt);
} catch (AuthorizationException e) {
requestContext.abortWith(
jakarta.ws.rs.core.Response.status(e.getStatusCode())
.entity("{\"error\": \"" + e.getMessage() + "\"}")
.build()
);
} catch (Exception e) {
requestContext.abortWith(
jakarta.ws.rs.core.Response.status(401)
.entity("{\"error\": \"Invalid token\"}")
.build()
);
}
}
}
private void verifyPayload(JsonWebToken jwt) {
// Implementa aquí tu lógica de verificación basada en el modelo de permisos
// Esto se mostrará en la sección de modelos de permisos más abajo
}
}
micronaut:
security:
authentication: bearer
token:
jwt:
signatures:
jwks:
logto:
url: ${JWKS_URI:https://your-tenant.logto.app/oidc/jwks}
import io.micronaut.security.token.Claims;
import io.micronaut.security.token.validator.TokenValidator;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@Singleton
public class JwtClaimsValidator implements TokenValidator {
@Override
public Publisher<Boolean> validateToken(String token, Claims claims) {
return Mono.fromCallable(() -> {
try {
verifyPayload(claims);
return true;
} catch (AuthorizationException e) {
throw new RuntimeException(e.getMessage());
}
});
}
private void verifyPayload(Claims claims) {
// Implementa aquí tu lógica de verificación basada en el modelo de permisos
// Esto se mostrará en la sección de modelos de permisos más abajo
}
}
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.WebClient;
public class JwtAuthHandler implements Handler<RoutingContext> {
private final JWTAuth jwtAuth;
private final WebClient webClient;
public JwtAuthHandler(io.vertx.core.Vertx vertx) {
this.webClient = WebClient.create(vertx);
this.jwtAuth = JWTAuth.create(vertx, new JWTAuthOptions());
// Obtener JWKS y configurar la autenticación JWT
fetchJWKS().onSuccess(jwks -> {
// Configura JWKS (simplificado - puede que necesites un parser JWKS adecuado)
});
}
@Override
public void handle(RoutingContext context) {
String authHeader = context.request().getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
context.response()
.setStatusCode(401)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"Authorization header missing or invalid\"}");
return;
}
String token = authHeader.substring(7);
jwtAuth.authenticate(new JsonObject().put("token", token))
.onSuccess(user -> {
try {
JsonObject principal = user.principal();
verifyPayload(principal);
// Almacena el principal JWT en el contexto para acceso en los handlers
context.put("auth", principal);
context.next();
} catch (AuthorizationException e) {
context.response()
.setStatusCode(e.getStatusCode())
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"" + e.getMessage() + "\"}");
} catch (Exception e) {
context.response()
.setStatusCode(401)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"Invalid token\"}");
}
})
.onFailure(err -> {
context.response()
.setStatusCode(401)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"Invalid token\"}");
});
}
private Future<JsonObject> fetchJWKS() {
return webClient.getAbs(AuthConstants.JWKS_URI)
.send()
.map(response -> response.bodyAsJsonObject());
}
private void verifyPayload(JsonObject principal) {
// Implementa aquí tu lógica de verificación basada en el modelo de permisos
// Esto se mostrará en la sección de modelos de permisos más abajo
}
}
De acuerdo a tu modelo de permisos, puede que necesites adoptar diferentes lógicas de verificación:
- Recursos de API globales
- Permisos de organización (no API)
- Recursos de API a nivel de organización
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
private void verifyPayload(Jwt jwt) {
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
List<String> audiences = jwt.getAudience();
if (!audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}
// Verifica los alcances requeridos para recursos de API globales
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Reemplaza por tus alcances requeridos
String scopes = jwt.getClaimAsString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}
}
private void verifyPayload(JsonWebToken jwt) {
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
if (!jwt.getAudience().contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}
// Verifica los alcances requeridos para recursos de API globales
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Reemplaza por tus alcances requeridos
String scopes = jwt.getClaim("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}
}
private void verifyPayload(Claims claims) {
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
List<String> audiences = (List<String>) claims.get("aud");
if (audiences == null || !audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}
// Verifica los alcances requeridos para recursos de API globales
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Reemplaza por tus alcances requeridos
String scopes = (String) claims.get("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}
}
private void verifyPayload(JsonObject principal) {
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
JsonArray audiences = principal.getJsonArray("aud");
if (audiences == null || !audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}
// Verifica los alcances requeridos para recursos de API globales
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Reemplaza por tus alcances requeridos
String scopes = principal.getString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}
}
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
private void verifyPayload(Jwt jwt) {
// Verifica que el claim de audiencia coincida con el formato de organización
List<String> audiences = jwt.getAudience();
boolean hasOrgAudience = audiences.stream()
.anyMatch(aud -> aud.startsWith("urn:logto:organization:"));
if (!hasOrgAudience) {
throw new AuthorizationException("Invalid audience for organization permissions");
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
String expectedOrgId = "your-organization-id"; // Extrae del contexto de la solicitud
String expectedAud = "urn:logto:organization:" + expectedOrgId;
if (!audiences.contains(expectedAud)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Verifica los alcances requeridos de la organización
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Reemplaza por tus alcances requeridos
String scopes = jwt.getClaimAsString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization scope");
}
}
private void verifyPayload(JsonWebToken jwt) {
// Verifica que el claim de audiencia coincida con el formato de organización
boolean hasOrgAudience = jwt.getAudience().stream()
.anyMatch(aud -> aud.startsWith("urn:logto:organization:"));
if (!hasOrgAudience) {
throw new AuthorizationException("Invalid audience for organization permissions");
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
String expectedOrgId = "your-organization-id"; // Extrae del contexto de la solicitud
String expectedAud = "urn:logto:organization:" + expectedOrgId;
if (!jwt.getAudience().contains(expectedAud)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Verifica los alcances requeridos de la organización
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Reemplaza por tus alcances requeridos
String scopes = jwt.getClaim("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization scope");
}
}
private void verifyPayload(Claims claims) {
// Verifica que el claim de audiencia coincida con el formato de organización
List<String> audiences = (List<String>) claims.get("aud");
boolean hasOrgAudience = audiences != null && audiences.stream()
.anyMatch(aud -> aud.startsWith("urn:logto:organization:"));
if (!hasOrgAudience) {
throw new AuthorizationException("Invalid audience for organization permissions");
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
String expectedOrgId = "your-organization-id"; // Extrae del contexto de la solicitud
String expectedAud = "urn:logto:organization:" + expectedOrgId;
if (!audiences.contains(expectedAud)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Verifica los alcances requeridos de la organización
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Reemplaza por tus alcances requeridos
String scopes = (String) claims.get("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization scope");
}
}
private void verifyPayload(JsonObject principal) {
// Verifica que el claim de audiencia coincida con el formato de organización
JsonArray audiences = principal.getJsonArray("aud");
boolean hasOrgAudience = false;
if (audiences != null) {
for (Object aud : audiences) {
if (aud.toString().startsWith("urn:logto:organization:")) {
hasOrgAudience = true;
break;
}
}
}
if (!hasOrgAudience) {
throw new AuthorizationException("Invalid audience for organization permissions");
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
String expectedOrgId = "your-organization-id"; // Extrae del contexto de la solicitud
String expectedAud = "urn:logto:organization:" + expectedOrgId;
if (!audiences.contains(expectedAud)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Verifica los alcances requeridos de la organización
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Reemplaza por tus alcances requeridos
String scopes = principal.getString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization scope");
}
}
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
private void verifyPayload(Jwt jwt) {
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
List<String> audiences = jwt.getAudience();
if (!audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience for organization-level API resources");
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
String expectedOrgId = "your-organization-id"; // Extrae del contexto de la solicitud
String orgId = jwt.getClaimAsString("organization_id");
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Verifica los alcances requeridos para recursos de API a nivel de organización
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Reemplaza por tus alcances requeridos
String scopes = jwt.getClaimAsString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization-level API scopes");
}
}
private void verifyPayload(JsonWebToken jwt) {
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
if (!jwt.getAudience().contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience for organization-level API resources");
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
String expectedOrgId = "your-organization-id"; // Extrae del contexto de la solicitud
String orgId = jwt.getClaim("organization_id");
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Verifica los alcances requeridos para recursos de API a nivel de organización
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Reemplaza por tus alcances requeridos
String scopes = jwt.getClaim("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization-level API scopes");
}
}
private void verifyPayload(Claims claims) {
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
List<String> audiences = (List<String>) claims.get("aud");
if (audiences == null || !audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience for organization-level API resources");
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
String expectedOrgId = "your-organization-id"; // Extrae del contexto de la solicitud
String orgId = (String) claims.get("organization_id");
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Verifica los alcances requeridos para recursos de API a nivel de organización
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Reemplaza por tus alcances requeridos
String scopes = (String) claims.get("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization-level API scopes");
}
}
private void verifyPayload(JsonObject principal) {
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
JsonArray audiences = principal.getJsonArray("aud");
if (audiences == null || !audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience for organization-level API resources");
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
String expectedOrgId = "your-organization-id"; // Extrae del contexto de la solicitud
String orgId = principal.getString("organization_id");
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Verifica los alcances requeridos para recursos de API a nivel de organización
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Reemplaza por tus alcances requeridos
String scopes = principal.getString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization-level API scopes");
}
}
Agrega el paquete NuGet requerido para la autenticación JWT:
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
Crea un servicio de validación para manejar la validación de tokens:
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using YourApiNamespace.Exceptions;
namespace YourApiNamespace.Services
{
public interface IJwtValidationService
{
Task ValidateTokenAsync(TokenValidatedContext context);
}
public class JwtValidationService : IJwtValidationService
{
public async Task ValidateTokenAsync(TokenValidatedContext context)
{
var principal = context.Principal!;
try
{
// Agrega aquí tu lógica de validación basada en el modelo de permisos
ValidatePayload(principal);
}
catch (AuthorizationException)
{
throw; // Re-lanzar excepciones de autorización
}
catch (Exception ex)
{
throw new AuthorizationException($"La validación del token falló: {ex.Message}", 401);
}
}
private void ValidatePayload(ClaimsPrincipal principal)
{
// Implementa aquí tu lógica de verificación basada en el modelo de permisos
// Esto se mostrará en la sección de modelos de permisos más abajo
}
}
}
Configura la autenticación JWT en tu Program.cs
:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using YourApiNamespace.Services;
using YourApiNamespace.Exceptions;
var builder = WebApplication.CreateBuilder(args);
// Agrega servicios al contenedor
builder.Services.AddControllers();
builder.Services.AddScoped<IJwtValidationService, JwtValidationService>();
// Configura la autenticación JWT
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = AuthConstants.Issuer;
options.MetadataAddress = $"{AuthConstants.Issuer}/.well-known/openid_configuration";
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = AuthConstants.Issuer,
ValidateAudience = false, // Validaremos la audiencia manualmente según el modelo de permisos
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var validationService = context.HttpContext.RequestServices
.GetRequiredService<IJwtValidationService>();
await validationService.ValidateTokenAsync(context);
},
OnAuthenticationFailed = context =>
{
// Manejar errores de la librería JWT como 401
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
context.Response.WriteAsync($"{{\"error\": \"Token inválido\"}}");
context.HandleResponse();
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
// Manejo global de errores para fallos de autenticación / autorización
app.Use(async (context, next) =>
{
try
{
await next();
}
catch (AuthorizationException ex)
{
context.Response.StatusCode = ex.StatusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync($"{{\"error\": \"{ex.Message}\"}}");
}
});
// Configura el pipeline de solicitudes HTTP
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
De acuerdo con tu modelo de permisos, implementa la lógica de validación apropiada en JwtValidationService
:
- Recursos de API globales
- Permisos de organización (no API)
- Recursos de API a nivel de organización
private void ValidatePayload(ClaimsPrincipal principal)
{
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
var audiences = principal.FindAll("aud").Select(c => c.Value).ToList();
if (!audiences.Contains("https://your-api-resource-indicator"))
{
throw new AuthorizationException("Audiencia inválida");
}
// Verifica los alcances requeridos para recursos de API globales
var requiredScopes = new[] { "api:read", "api:write" }; // Reemplaza con tus alcances requeridos
var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
{
throw new AuthorizationException("Alcance insuficiente");
}
}
private void ValidatePayload(ClaimsPrincipal principal)
{
// Verifica que el claim de audiencia coincida con el formato de organización
var audiences = principal.FindAll("aud").Select(c => c.Value).ToList();
var hasOrgAudience = audiences.Any(aud => aud.StartsWith("urn:logto:organization:"));
if (!hasOrgAudience)
{
throw new AuthorizationException("Audiencia inválida para permisos de organización");
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
var expectedOrgId = "your-organization-id"; // Extrae del contexto de la solicitud
var expectedAud = $"urn:logto:organization:{expectedOrgId}";
if (!audiences.Contains(expectedAud))
{
throw new AuthorizationException("ID de organización no coincide");
}
// Verifica los alcances requeridos de la organización
var requiredScopes = new[] { "invite:users", "manage:settings" }; // Reemplaza con tus alcances requeridos
var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
{
throw new AuthorizationException("Alcance de organización insuficiente");
}
}
private void ValidatePayload(ClaimsPrincipal principal)
{
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
var audiences = principal.FindAll("aud").Select(c => c.Value).ToList();
if (!audiences.Contains("https://your-api-resource-indicator"))
{
throw new AuthorizationException("Audiencia inválida para recursos de API a nivel de organización");
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraerlo del contexto de la solicitud)
var expectedOrgId = "your-organization-id"; // Extrae del contexto de la solicitud
var orgId = principal.FindFirst("organization_id")?.Value;
if (!expectedOrgId.Equals(orgId))
{
throw new AuthorizationException("ID de organización no coincide");
}
// Verifica los alcances requeridos para recursos de API a nivel de organización
var requiredScopes = new[] { "api:read", "api:write" }; // Reemplaza con tus alcances requeridos
var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
{
throw new AuthorizationException("Alcances insuficientes para recursos de API a nivel de organización");
}
}
Usamos firebase/php-jwt para validar JWTs. Instálalo usando Composer:
- Laravel
- Symfony
- Slim
composer require firebase/php-jwt
composer require firebase/php-jwt
composer require firebase/php-jwt slim/slim:"4.*" slim/psr7
Primero, añade estas utilidades compartidas para manejar la validación de JWT:
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
use Firebase\JWT\Key;
class JwtValidator
{
use AuthHelpers;
private static ?array $jwks = null;
public static function fetchJwks(): array
{
if (self::$jwks === null) {
$jwksData = file_get_contents(AuthConstants::JWKS_URI);
if ($jwksData === false) {
throw new AuthorizationException('No se pudo obtener el JWKS', 401);
}
self::$jwks = json_decode($jwksData, true);
}
return self::$jwks;
}
public static function validateJwtToken(string $token): array
{
try {
$jwks = self::fetchJwks();
$keys = JWK::parseKeySet($jwks);
$decoded = JWT::decode($token, $keys);
$payload = (array) $decoded;
// Verificar emisor
if (($payload['iss'] ?? '') !== AuthConstants::ISSUER) {
throw new AuthorizationException('Emisor inválido', 401);
}
self::verifyPayload($payload);
return $payload;
} catch (AuthorizationException $e) {
throw $e;
} catch (Exception $e) {
throw new AuthorizationException('Token inválido: ' . $e->getMessage(), 401);
}
}
public static function createAuthInfo(array $payload): AuthInfo
{
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
$audience = $payload['aud'] ?? [];
if (is_string($audience)) {
$audience = [$audience];
}
return new AuthInfo(
sub: $payload['sub'],
clientId: $payload['client_id'] ?? null,
organizationId: $payload['organization_id'] ?? null,
scopes: $scopes,
audience: $audience
);
}
private static function verifyPayload(array $payload): void
{
// Implementa aquí tu lógica de verificación basada en el modelo de permisos
// Esto se mostrará en la sección de modelos de permisos más abajo
}
}
Luego, implementa el middleware para verificar el token de acceso:
- Laravel
- Symfony
- Slim
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class VerifyAccessToken
{
use AuthHelpers;
public function handle(Request $request, Closure $next): Response
{
try {
$token = $this->extractBearerToken($request->headers->all());
$payload = JwtValidator::validateJwtToken($token);
// Almacenar la información de autenticación en los atributos de la solicitud para uso general
$request->attributes->set('auth', JwtValidator::createAuthInfo($payload));
return $next($request);
} catch (AuthorizationException $e) {
return response()->json(['error' => $e->getMessage()], $e->statusCode);
}
}
}
Registra el middleware en app/Http/Kernel.php
:
protected $middlewareAliases = [
// ... otros middleware
'auth.token' => \App\Http\Middleware\VerifyAccessToken::class,
];
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class JwtAuthenticator extends AbstractAuthenticator
{
use AuthHelpers;
public function supports(Request $request): ?bool
{
return $request->headers->has('authorization');
}
public function authenticate(Request $request): Passport
{
try {
$token = $this->extractBearerToken($request->headers->all());
$payload = JwtValidator::validateJwtToken($token);
$authInfo = JwtValidator::createAuthInfo($payload);
// Almacenar la información de autenticación en los atributos de la solicitud para uso general
$request->attributes->set('auth', $authInfo);
return new SelfValidatingPassport(new UserBadge($payload['sub']));
} catch (AuthorizationException $e) {
throw new AuthenticationException($e->getMessage());
}
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null; // Continuar al controlador
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse(['error' => $exception->getMessage()], Response::HTTP_UNAUTHORIZED);
}
}
Configura la seguridad en config/packages/security.yaml
:
security:
firewalls:
api:
pattern: ^/api/protected
stateless: true
custom_authenticators:
- App\Security\JwtAuthenticator
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Response;
class JwtMiddleware implements MiddlewareInterface
{
use AuthHelpers;
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
$headers = $request->getHeaders();
$token = $this->extractBearerToken($headers);
$payload = JwtValidator::validateJwtToken($token);
// Almacenar la información de autenticación en los atributos de la solicitud para uso general
$request = $request->withAttribute('auth', JwtValidator::createAuthInfo($payload));
return $handler->handle($request);
} catch (AuthorizationException $e) {
$response = new Response();
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response
->withHeader('Content-Type', 'application/json')
->withStatus($e->statusCode);
}
}
}
De acuerdo con tu modelo de permisos, implementa la lógica de verificación adecuada en JwtValidator
:
- Recursos de API globales
- Permisos de organización (no API)
- Recursos de API a nivel de organización
private static function verifyPayload(array $payload): void
{
// Verifica que el reclamo de audiencia coincida con tu indicador de recurso de API
$audiences = $payload['aud'] ?? [];
if (is_string($audiences)) {
$audiences = [$audiences];
}
if (!in_array('https://your-api-resource-indicator', $audiences)) {
throw new AuthorizationException('Audiencia inválida');
}
// Verifica los alcances requeridos para recursos de API globales
$requiredScopes = ['api:read', 'api:write']; // Reemplaza con tus alcances requeridos
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $scopes)) {
throw new AuthorizationException('Alcance insuficiente');
}
}
}
private static function verifyPayload(array $payload): void
{
// Verifica que el reclamo de audiencia coincida con el formato de organización
$audiences = $payload['aud'] ?? [];
if (is_string($audiences)) {
$audiences = [$audiences];
}
$hasOrgAudience = false;
foreach ($audiences as $aud) {
if (str_starts_with($aud, 'urn:logto:organization:')) {
$hasOrgAudience = true;
break;
}
}
if (!$hasOrgAudience) {
throw new AuthorizationException('Audiencia inválida para permisos de organización');
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraer esto del contexto de la solicitud)
$expectedOrgId = 'your-organization-id'; // Extraer del contexto de la solicitud
$expectedAud = "urn:logto:organization:{$expectedOrgId}";
if (!in_array($expectedAud, $audiences)) {
throw new AuthorizationException('ID de organización no coincide');
}
// Verifica los alcances requeridos de la organización
$requiredScopes = ['invite:users', 'manage:settings']; // Reemplaza con tus alcances requeridos
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $scopes)) {
throw new AuthorizationException('Alcance de organización insuficiente');
}
}
}
private static function verifyPayload(array $payload): void
{
// Verifica que el reclamo de audiencia coincida con tu indicador de recurso de API
$audiences = $payload['aud'] ?? [];
if (is_string($audiences)) {
$audiences = [$audiences];
}
if (!in_array('https://your-api-resource-indicator', $audiences)) {
throw new AuthorizationException('Audiencia inválida para recursos de API a nivel de organización');
}
// Verifica que el ID de la organización coincida con el contexto (puede que necesites extraer esto del contexto de la solicitud)
$expectedOrgId = 'your-organization-id'; // Extraer del contexto de la solicitud
$orgId = $payload['organization_id'] ?? null;
if ($expectedOrgId !== $orgId) {
throw new AuthorizationException('ID de organización no coincide');
}
// Verifica los alcances requeridos para recursos de API a nivel de organización
$requiredScopes = ['api:read', 'api:write']; // Reemplaza con tus alcances requeridos
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $scopes)) {
throw new AuthorizationException('Alcances de API a nivel de organización insuficientes');
}
}
}
Usamos la gema jwt para validar JWTs. Añádela a tu Gemfile:
gem 'jwt'
# net-http es parte de la biblioteca estándar de Ruby desde Ruby 2.7, no es necesario añadirlo explícitamente
Luego ejecuta:
bundle install
Primero, añade estas utilidades compartidas para manejar JWKS y la validación de tokens:
require 'jwt'
require 'net/http'
require 'json'
class JwtValidator
include AuthHelpers
def self.fetch_jwks
@jwks ||= begin
uri = URI(AuthConstants::JWKS_URI)
response = Net::HTTP.get_response(uri)
raise AuthorizationError.new('No se pudo obtener JWKS', 401) unless response.is_a?(Net::HTTPSuccess)
jwks_data = JSON.parse(response.body)
JWT::JWK::Set.new(jwks_data)
end
end
def self.validate_jwt_token(token)
jwks = fetch_jwks
# Deja que la biblioteca JWT maneje la detección del algoritmo desde JWKS
decoded_token = JWT.decode(token, nil, true, {
iss: AuthConstants::ISSUER,
verify_iss: true,
verify_aud: false, # Verificaremos la audiencia manualmente según el modelo de permisos
jwks: jwks
})[0]
decoded_token
end
def self.create_auth_info(payload)
scopes = payload['scope']&.split(' ') || []
audience = payload['aud'] || []
AuthInfo.new(
payload['sub'],
payload['client_id'],
payload['organization_id'],
scopes,
audience
)
end
def self.verify_payload(payload)
# Implementa aquí tu lógica de verificación según el modelo de permisos
# Esto se mostrará en la sección de modelos de permisos más abajo
end
end
Luego, implementa el middleware para verificar el token de acceso:
- Rails
- Sinatra
- Grape
module JwtAuthentication
extend ActiveSupport::Concern
include AuthHelpers
included do
before_action :verify_access_token, only: [:protected_action] # Añade acciones específicas
end
private
def verify_access_token
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt_token(token)
JwtValidator.verify_payload(decoded_token)
# Guarda la información de autenticación para uso general
@auth = JwtValidator.create_auth_info(decoded_token)
rescue AuthorizationError => e
render json: { error: e.message }, status: e.status
rescue JWT::DecodeError, JWT::VerificationError, JWT::ExpiredSignature => e
render json: { error: 'Token inválido' }, status: 401
end
end
end
class AuthMiddleware
include AuthHelpers
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
# Solo protege rutas específicas
if request.path.start_with?('/api/protected')
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt_token(token)
JwtValidator.verify_payload(decoded_token)
# Guarda la información de autenticación en env para uso general
env['auth'] = JwtValidator.create_auth_info(decoded_token)
rescue AuthorizationError => e
return [e.status, { 'Content-Type' => 'application/json' }, [{ error: e.message }.to_json]]
rescue JWT::DecodeError, JWT::VerificationError, JWT::ExpiredSignature => e
return [401, { 'Content-Type' => 'application/json' }, [{ error: 'Token inválido' }.to_json]]
end
end
@app.call(env)
end
end
module GrapeAuthHelpers
include AuthHelpers
def authenticate_user!
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt_token(token)
JwtValidator.verify_payload(decoded_token)
# Guarda la información de autenticación para uso general
@auth = JwtValidator.create_auth_info(decoded_token)
rescue AuthorizationError => e
error!({ error: e.message }, e.status)
rescue JWT::DecodeError, JWT::VerificationError, JWT::ExpiredSignature => e
error!({ error: 'Token inválido' }, 401)
end
end
def auth
@auth
end
end
Según tu modelo de permisos, implementa la lógica de verificación apropiada en JwtValidator
:
- Recursos de API globales
- Permisos de organización (no API)
- Recursos de API a nivel de organización
def self.verify_payload(payload)
# Verifica que el reclamo de audiencia coincida con tu indicador de recurso de API
audiences = payload['aud'] || []
unless audiences.include?('https://your-api-resource-indicator')
raise AuthorizationError.new('Audiencia inválida')
end
# Verifica los alcances requeridos para recursos de API globales
required_scopes = ['api:read', 'api:write'] # Reemplaza con tus alcances requeridos
token_scopes = payload['scope']&.split(' ') || []
unless required_scopes.all? { |scope| token_scopes.include?(scope) }
raise AuthorizationError.new('Alcance insuficiente')
end
end
def self.verify_payload(payload)
# Verifica que el reclamo de audiencia coincida con el formato de organización
audiences = payload['aud'] || []
has_org_audience = audiences.any? { |aud| aud.start_with?('urn:logto:organization:') }
unless has_org_audience
raise AuthorizationError.new('Audiencia inválida para permisos de organización')
end
# Verifica que el ID de la organización coincida con el contexto (puede que necesites extraer esto del contexto de la solicitud)
expected_org_id = 'your-organization-id' # Extrae del contexto de la solicitud
expected_aud = "urn:logto:organization:#{expected_org_id}"
unless audiences.include?(expected_aud)
raise AuthorizationError.new('ID de organización no coincide')
end
# Verifica los alcances requeridos de la organización
required_scopes = ['invite:users', 'manage:settings'] # Reemplaza con tus alcances requeridos
token_scopes = payload['scope']&.split(' ') || []
unless required_scopes.all? { |scope| token_scopes.include?(scope) }
raise AuthorizationError.new('Alcance de organización insuficiente')
end
end
def self.verify_payload(payload)
# Verifica que el reclamo de audiencia coincida con tu indicador de recurso de API
audiences = payload['aud'] || []
unless audiences.include?('https://your-api-resource-indicator')
raise AuthorizationError.new('Audiencia inválida para recursos de API a nivel de organización')
end
# Verifica que el ID de la organización coincida con el contexto (puede que necesites extraer esto del contexto de la solicitud)
expected_org_id = 'your-organization-id' # Extrae del contexto de la solicitud
org_id = payload['organization_id']
unless expected_org_id == org_id
raise AuthorizationError.new('ID de organización no coincide')
end
# Verifica los alcances requeridos para recursos de API a nivel de organización
required_scopes = ['api:read', 'api:write'] # Reemplaza con tus alcances requeridos
token_scopes = payload['scope']&.split(' ') || []
unless required_scopes.all? { |scope| token_scopes.include?(scope) }
raise AuthorizationError.new('Alcances insuficientes para recursos de API a nivel de organización')
end
end
Usamos jsonwebtoken para validar JWTs. Añade las dependencias requeridas a tu Cargo.toml
:
- Axum
- Actix Web
- Rocket
[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors"] }
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
[dependencies]
actix-web = "4.0"
tokio = { version = "1.0", features = ["full"] }
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
[dependencies]
rocket = { version = "0.5", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
Primero, añade estas utilidades compartidas para manejar la validación de JWT:
use crate::{AuthInfo, AuthorizationError, ISSUER, JWKS_URI};
use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
use serde_json::Value;
use std::collections::HashMap;
pub struct JwtValidator {
jwks: HashMap<String, DecodingKey>,
}
impl JwtValidator {
pub async fn new() -> Result<Self, AuthorizationError> {
let jwks = Self::fetch_jwks().await?;
Ok(Self { jwks })
}
async fn fetch_jwks() -> Result<HashMap<String, DecodingKey>, AuthorizationError> {
let response = reqwest::get(JWKS_URI).await.map_err(|e| {
AuthorizationError::with_status(format!("Failed to fetch JWKS: {}", e), 401)
})?;
let jwks: Value = response.json().await.map_err(|e| {
AuthorizationError::with_status(format!("Failed to parse JWKS: {}", e), 401)
})?;
let mut keys = HashMap::new();
if let Some(keys_array) = jwks["keys"].as_array() {
for key in keys_array {
if let (Some(kid), Some(kty), Some(n), Some(e)) = (
key["kid"].as_str(),
key["kty"].as_str(),
key["n"].as_str(),
key["e"].as_str(),
) {
if kty == "RSA" {
if let Ok(decoding_key) = DecodingKey::from_rsa_components(n, e) {
keys.insert(kid.to_string(), decoding_key);
}
}
}
}
}
if keys.is_empty() {
return Err(AuthorizationError::with_status("No valid keys found in JWKS", 401));
}
Ok(keys)
}
pub fn validate_jwt_token(&self, token: &str) -> Result<AuthInfo, AuthorizationError> {
let header = decode_header(token).map_err(|e| {
AuthorizationError::with_status(format!("Invalid token header: {}", e), 401)
})?;
let kid = header.kid.ok_or_else(|| {
AuthorizationError::with_status("Token missing kid claim", 401)
})?;
let key = self.jwks.get(&kid).ok_or_else(|| {
AuthorizationError::with_status("Unknown key ID", 401)
})?;
let mut validation = Validation::new(Algorithm::RS256);
validation.set_issuer(&[ISSUER]);
validation.validate_aud = false; // Verificaremos la audiencia manualmente
let token_data = decode::<Value>(token, key, &validation).map_err(|e| {
AuthorizationError::with_status(format!("Invalid token: {}", e), 401)
})?;
let claims = token_data.claims;
self.verify_payload(&claims)?;
Ok(self.create_auth_info(claims))
}
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
// Implementa aquí tu lógica de verificación basada en el modelo de permisos
// Esto se mostrará en la sección de modelos de permisos más abajo
Ok(())
}
fn create_auth_info(&self, claims: Value) -> AuthInfo {
let scopes = claims["scope"]
.as_str()
.map(|s| s.split(' ').map(|s| s.to_string()).collect())
.unwrap_or_default();
let audience = match &claims["aud"] {
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect(),
Value::String(s) => vec![s.clone()],
_ => vec![],
};
AuthInfo::new(
claims["sub"].as_str().unwrap_or_default().to_string(),
claims["client_id"].as_str().map(|s| s.to_string()),
claims["organization_id"].as_str().map(|s| s.to_string()),
scopes,
audience,
)
}
}
Luego, implementa el middleware para verificar el token de acceso (access token):
- Axum
- Actix Web
- Rocket
use crate::{AuthInfo, AuthorizationError, extract_bearer_token};
use crate::jwt_validator::JwtValidator;
use axum::{
extract::Request,
http::{HeaderMap, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
Extension, Json,
};
use serde_json::json;
use std::sync::Arc;
pub async fn jwt_middleware(
Extension(validator): Extension<Arc<JwtValidator>>,
headers: HeaderMap,
mut request: Request,
next: Next,
) -> Result<Response, AuthorizationError> {
let authorization = headers
.get("authorization")
.and_then(|h| h.to_str().ok());
let token = extract_bearer_token(authorization)?;
let auth_info = validator.validate_jwt_token(token)?;
// Almacena la información de autenticación en las extensiones de la solicitud para uso genérico
request.extensions_mut().insert(auth_info);
Ok(next.run(request).await)
}
impl IntoResponse for AuthorizationError {
fn into_response(self) -> Response {
let status = StatusCode::from_u16(self.status_code).unwrap_or(StatusCode::FORBIDDEN);
(status, Json(json!({ "error": self.message }))).into_response()
}
}
use crate::{AuthInfo, AuthorizationError, extract_bearer_token};
use crate::jwt_validator::JwtValidator;
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
web, Error, HttpMessage, HttpResponse,
};
use futures::future::{ok, Ready};
use std::sync::Arc;
pub struct JwtMiddleware {
validator: Arc<JwtValidator>,
}
impl JwtMiddleware {
pub fn new(validator: Arc<JwtValidator>) -> Self {
Self { validator }
}
}
impl<S, B> Transform<S, ServiceRequest> for JwtMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = JwtMiddlewareService<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(JwtMiddlewareService {
service,
validator: self.validator.clone(),
})
}
}
pub struct JwtMiddlewareService<S> {
service: S,
validator: Arc<JwtValidator>,
}
impl<S, B> Service<ServiceRequest> for JwtMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = futures::future::LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let validator = self.validator.clone();
Box::pin(async move {
let authorization = req
.headers()
.get("authorization")
.and_then(|h| h.to_str().ok());
match extract_bearer_token(authorization)
.and_then(|token| validator.validate_jwt_token(token))
{
Ok(auth_info) => {
// Almacena la información de autenticación en las extensiones de la solicitud para uso genérico
req.extensions_mut().insert(auth_info);
let fut = self.service.call(req);
fut.await
}
Err(e) => {
let response = HttpResponse::build(
actix_web::http::StatusCode::from_u16(e.status_code)
.unwrap_or(actix_web::http::StatusCode::FORBIDDEN),
)
.json(serde_json::json!({ "error": e.message }));
Ok(req.into_response(response))
}
}
})
}
}
use crate::{AuthInfo, AuthorizationError, extract_bearer_token};
use crate::jwt_validator::JwtValidator;
use rocket::{
http::Status,
outcome::Outcome,
request::{self, FromRequest, Request},
State,
};
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthInfo {
type Error = AuthorizationError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let validator = match req.guard::<&State<JwtValidator>>().await {
Outcome::Success(validator) => validator,
Outcome::Failure((status, _)) => {
return Outcome::Failure((
status,
AuthorizationError::with_status("JWT validator not found", 500),
))
}
Outcome::Forward(()) => {
return Outcome::Forward(())
}
};
let authorization = req.headers().get_one("authorization");
match extract_bearer_token(authorization)
.and_then(|token| validator.validate_jwt_token(token))
{
Ok(auth_info) => Outcome::Success(auth_info),
Err(e) => {
let status = Status::from_code(e.status_code).unwrap_or(Status::Forbidden);
Outcome::Failure((status, e))
}
}
}
}
De acuerdo con tu modelo de permisos, implementa la lógica de verificación apropiada en JwtValidator
:
- Recursos de API globales
- Permisos de organización (no API)
- Recursos de API a nivel de organización
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
let audiences = match &claims["aud"] {
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>(),
Value::String(s) => vec![s.as_str()],
_ => vec![],
};
if !audiences.contains(&"https://your-api-resource-indicator") {
return Err(AuthorizationError::new("Invalid audience"));
}
// Verifica los alcances requeridos para recursos de API globales
let required_scopes = vec!["api:read", "api:write"]; // Reemplaza con tus alcances requeridos
let scopes = claims["scope"]
.as_str()
.map(|s| s.split(' ').collect::<Vec<_>>())
.unwrap_or_default();
for required_scope in &required_scopes {
if !scopes.contains(required_scope) {
return Err(AuthorizationError::new("Insufficient scope"));
}
}
Ok(())
}
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
// Verifica que el claim de audiencia coincida con el formato de organización
let audiences = match &claims["aud"] {
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>(),
Value::String(s) => vec![s.as_str()],
_ => vec![],
};
let has_org_audience = audiences.iter().any(|aud| aud.starts_with("urn:logto:organization:"));
if !has_org_audience {
return Err(AuthorizationError::new("Invalid audience for organization permissions"));
}
// Verifica que el ID de la organización coincida con el contexto (puedes necesitar extraer esto del contexto de la solicitud)
let expected_org_id = "your-organization-id"; // Extrae del contexto de la solicitud
let expected_aud = format!("urn:logto:organization:{}", expected_org_id);
if !audiences.contains(&expected_aud.as_str()) {
return Err(AuthorizationError::new("Organization ID mismatch"));
}
// Verifica los alcances requeridos de la organización
let required_scopes = vec!["invite:users", "manage:settings"]; // Reemplaza con tus alcances requeridos
let scopes = claims["scope"]
.as_str()
.map(|s| s.split(' ').collect::<Vec<_>>())
.unwrap_or_default();
for required_scope in &required_scopes {
if !scopes.contains(required_scope) {
return Err(AuthorizationError::new("Insufficient organization scope"));
}
}
Ok(())
}
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
// Verifica que el claim de audiencia coincida con tu indicador de recurso de API
let audiences = match &claims["aud"] {
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>(),
Value::String(s) => vec![s.as_str()],
_ => vec![],
};
if !audiences.contains(&"https://your-api-resource-indicator") {
return Err(AuthorizationError::new("Invalid audience for organization-level API resources"));
}
// Verifica que el ID de la organización coincida con el contexto (puedes necesitar extraer esto del contexto de la solicitud)
let expected_org_id = "your-organization-id"; // Extrae del contexto de la solicitud
let org_id = claims["organization_id"].as_str().unwrap_or_default();
if expected_org_id != org_id {
return Err(AuthorizationError::new("Organization ID mismatch"));
}
// Verifica los alcances requeridos para recursos de API a nivel de organización
let required_scopes = vec!["api:read", "api:write"]; // Reemplaza con tus alcances requeridos
let scopes = claims["scope"]
.as_str()
.map(|s| s.split(' ').collect::<Vec<_>>())
.unwrap_or_default();
for required_scope in &required_scopes {
if !scopes.contains(required_scope) {
return Err(AuthorizationError::new("Insufficient organization-level API scopes"));
}
}
Ok(())
}
Paso 4: Aplica el middleware a tu API
Aplica el middleware a tus rutas de API protegidas.
- Node.js
- Python
- Go
- Java
- .NET
- PHP
- Ruby
- Rust
- Express.js
- Koa.js
- Fastify
- Hapi.js
- NestJS
import express from 'express';
import { verifyAccessToken } from './auth-middleware.js';
const app = express();
app.get('/api/protected', verifyAccessToken, (req, res) => {
// Accede a la información de autenticación directamente desde req.auth
res.json({ auth: req.auth });
});
app.get('/api/protected/detailed', verifyAccessToken, (req, res) => {
// Tu lógica para el endpoint protegido
res.json({
auth: req.auth,
message: 'Datos protegidos accedidos correctamente',
});
});
app.listen(3000);
import Koa from 'koa';
import Router from '@koa/router';
import { koaVerifyAccessToken } from './auth-middleware.js';
const app = new Koa();
const router = new Router();
router.get('/api/protected', koaVerifyAccessToken, (ctx) => {
// Accede a la información de autenticación directamente desde ctx.state.auth
ctx.body = { auth: ctx.state.auth };
});
router.get('/api/protected/detailed', koaVerifyAccessToken, (ctx) => {
// Tu lógica para el endpoint protegido
ctx.body = {
auth: ctx.state.auth,
message: 'Datos protegidos accedidos correctamente',
};
});
app.use(router.routes());
app.listen(3000);
import Fastify from 'fastify';
import { fastifyVerifyAccessToken } from './auth-middleware.js';
const fastify = Fastify();
fastify.get('/api/protected', { preHandler: fastifyVerifyAccessToken }, (request, reply) => {
// Accede a la información de autenticación directamente desde request.auth
reply.send({ auth: request.auth });
});
fastify.get(
'/api/protected/detailed',
{ preHandler: fastifyVerifyAccessToken },
(request, reply) => {
// Tu lógica para el endpoint protegido
reply.send({
auth: request.auth,
message: 'Datos protegidos accedidos correctamente',
});
}
);
fastify.listen({ port: 3000 });
import Hapi from '@hapi/hapi';
import { hapiVerifyAccessToken } from './auth-middleware.js';
const server = Hapi.server({ port: 3000 });
server.route({
method: 'GET',
path: '/api/protected',
options: {
pre: [{ method: hapiVerifyAccessToken }],
handler: (request, h) => {
// Accede a la información de autenticación desde request.app.auth
return { auth: request.app.auth };
},
},
});
server.route({
method: 'GET',
path: '/api/protected/detailed',
options: {
pre: [{ method: hapiVerifyAccessToken }],
handler: (request, h) => {
// Tu lógica para el endpoint protegido
return {
auth: request.app.auth,
message: 'Datos protegidos accedidos correctamente',
};
},
},
});
await server.start();
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { AccessTokenGuard } from './access-token.guard.js';
@Controller('api')
export class ProtectedController {
@Get('protected')
@UseGuards(AccessTokenGuard)
getProtected(@Req() req: any) {
// Accede a la información de autenticación desde req.auth
return { auth: req.auth };
}
@Get('protected/detailed')
@UseGuards(AccessTokenGuard)
getDetailedProtected(@Req() req: any) {
// Tu lógica para el endpoint protegido
return {
auth: req.auth,
message: 'Datos protegidos accedidos correctamente',
};
}
}
- FastAPI
- Flask
- Django
- Django REST Framework
from fastapi import FastAPI, Depends
from auth_middleware import verify_access_token, AuthInfo
app = FastAPI()
@app.get("/api/protected")
async def protected_endpoint(auth: AuthInfo = Depends(verify_access_token)):
# Accede a la información de autenticación directamente desde el parámetro auth
return {"auth": auth.to_dict()}
@app.get("/api/protected/detailed")
async def detailed_endpoint(auth: AuthInfo = Depends(verify_access_token)):
# Tu lógica protegida del endpoint
return {
"auth": auth.to_dict(),
"message": "Datos protegidos accedidos correctamente"
}
from flask import Flask, g, jsonify
from auth_middleware import verify_access_token
app = Flask(__name__)
@app.route('/api/protected', methods=['GET'])
@verify_access_token
def protected_endpoint():
# Accede a la información de autenticación desde g.auth
return jsonify({"auth": g.auth.to_dict()})
@app.route('/api/protected/detailed', methods=['GET'])
@verify_access_token
def detailed_endpoint():
# Tu lógica protegida del endpoint
return jsonify({
"auth": g.auth.to_dict(),
"message": "Datos protegidos accedidos correctamente"
})
from django.http import JsonResponse
from auth_middleware import require_access_token
@require_access_token
def protected_view(request):
# Accede a la información de autenticación desde request.auth
return JsonResponse({"auth": request.auth.to_dict()})
@require_access_token
def detailed_view(request):
# Tu lógica protegida del endpoint
return JsonResponse({
"auth": request.auth.to_dict(),
"message": "Datos protegidos accedidos correctamente"
})
from rest_framework.decorators import api_view, authentication_classes
from rest_framework.response import Response
from auth_middleware import AccessTokenAuthentication
@api_view(['GET'])
@authentication_classes([AccessTokenAuthentication])
def protected_view(request):
# Accede a la información de autenticación desde request.user.auth
return Response({"auth": request.user.auth.to_dict()})
@api_view(['GET'])
@authentication_classes([AccessTokenAuthentication])
def detailed_view(request):
# Tu lógica protegida del endpoint
return Response({
"auth": request.user.auth.to_dict(),
"message": "Datos protegidos accedidos correctamente"
})
O usando vistas basadas en clases:
from rest_framework.views import APIView
from rest_framework.response import Response
from auth_middleware import AccessTokenAuthentication
class ProtectedView(APIView):
authentication_classes = [AccessTokenAuthentication]
def get(self, request):
# Accede a la información de autenticación desde request.user.auth
return Response({"auth": request.user.auth.to_dict()})
class DetailedView(APIView):
authentication_classes = [AccessTokenAuthentication]
def get(self, request):
# Tu lógica protegida del endpoint
return Response({
"auth": request.user.auth.to_dict(),
"message": "Datos protegidos accedidos correctamente"
})
- Gin
- Echo
- Fiber
- Chi
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
r := gin.Default()
// Aplica el middleware a las rutas protegidas
r.GET("/api/protected", VerifyAccessToken(), func(c *gin.Context) {
// Información del token de acceso directamente desde el contexto
tokenInterface, exists := c.Get("auth")
if !exists {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token not found"})
return
}
token := tokenInterface.(jwt.Token)
c.JSON(http.StatusOK, gin.H{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
})
})
r.Run(":8080")
}
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
e := echo.New()
// Aplica el middleware a las rutas protegidas
e.GET("/api/protected", func(c echo.Context) error {
// Información del token de acceso directamente desde el contexto
tokenInterface := c.Get("auth")
if tokenInterface == nil {
return c.JSON(http.StatusInternalServerError, echo.Map{"error": "Token not found"})
}
token := tokenInterface.(jwt.Token)
return c.JSON(http.StatusOK, echo.Map{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
})
}, VerifyAccessToken)
e.Start(":8080")
}
O usando grupos de rutas:
package main
import (
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
e := echo.New()
// Crea un grupo de rutas protegidas
api := e.Group("/api", VerifyAccessToken)
api.GET("/protected", func(c echo.Context) error {
// Información del token de acceso directamente desde el contexto
token := c.Get("auth").(jwt.Token)
return c.JSON(200, echo.Map{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
"message": "Acceso a datos protegidos realizado con éxito",
})
})
e.Start(":8080")
}
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
app := fiber.New()
// Aplica el middleware a las rutas protegidas
app.Get("/api/protected", VerifyAccessToken, func(c *fiber.Ctx) error {
// Información del token de acceso directamente desde locals
tokenInterface := c.Locals("auth")
if tokenInterface == nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Token not found"})
}
token := tokenInterface.(jwt.Token)
return c.JSON(fiber.Map{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
})
})
app.Listen(":8080")
}
O usando grupos de rutas:
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
app := fiber.New()
// Crea un grupo de rutas protegidas
api := app.Group("/api", VerifyAccessToken)
api.Get("/protected", func(c *fiber.Ctx) error {
// Información del token de acceso directamente desde locals
token := c.Locals("auth").(jwt.Token)
return c.JSON(fiber.Map{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
"message": "Acceso a datos protegidos realizado con éxito",
})
})
app.Listen(":8080")
}
package main
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
r := chi.NewRouter()
// Aplica el middleware a las rutas protegidas
r.With(VerifyAccessToken).Get("/api/protected", func(w http.ResponseWriter, r *http.Request) {
// Información del token de acceso directamente desde el contexto
tokenInterface := r.Context().Value(AuthContextKey)
if tokenInterface == nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": "Token not found"})
return
}
token := tokenInterface.(jwt.Token)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
})
})
http.ListenAndServe(":8080", r)
}
O usando grupos de rutas:
package main
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
r := chi.NewRouter()
// Crea un grupo de rutas protegidas
r.Route("/api", func(r chi.Router) {
r.Use(VerifyAccessToken)
r.Get("/protected", func(w http.ResponseWriter, r *http.Request) {
// Información del token de acceso directamente desde el contexto
token := r.Context().Value(AuthContextKey).(jwt.Token)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
"message": "Acceso a datos protegidos realizado con éxito",
})
})
})
http.ListenAndServe(":8080", r)
}
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@RestController
public class ProtectedController {
@GetMapping("/api/protected")
public Map<String, Object> protectedEndpoint(@AuthenticationPrincipal Jwt jwt) {
// Accede a la información del token de acceso directamente desde el JWT
String scopes = jwt.getClaimAsString("scope");
List<String> scopeList = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
return Map.of(
"sub", jwt.getSubject(),
"client_id", jwt.getClaimAsString("client_id"),
"organization_id", jwt.getClaimAsString("organization_id"),
"scopes", scopeList,
"audience", jwt.getAudience()
);
}
@GetMapping("/api/protected/detailed")
public Map<String, Object> detailedEndpoint(@AuthenticationPrincipal Jwt jwt) {
// Tu lógica del endpoint protegido
return Map.of(
"sub", jwt.getSubject(),
"client_id", jwt.getClaimAsString("client_id"),
"organization_id", jwt.getClaimAsString("organization_id"),
"scopes", jwt.getClaimAsString("scope"),
"audience", jwt.getAudience(),
"message", "Datos protegidos accedidos correctamente"
);
}
}
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()
);
}
@GET
@Path("/protected/detailed")
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Object> detailedEndpoint() {
// Tu lógica del endpoint protegido
String scopes = jwt.getClaim("scope");
return Map.of(
"sub", jwt.getSubject(),
"client_id", jwt.<String>getClaim("client_id"),
"organization_id", jwt.<String>getClaim("organization_id"),
"scopes", scopes,
"audience", jwt.getAudience(),
"message", "Datos protegidos accedidos correctamente"
);
}
}
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.rules.SecurityRule;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@Controller("/api")
@Secured(SecurityRule.IS_AUTHENTICATED)
public class ProtectedController {
@Get("/protected")
public Map<String, Object> protectedEndpoint(Authentication authentication) {
// Accede a la información del token de acceso directamente desde Authentication
String scopes = (String) authentication.getAttributes().get("scope");
List<String> scopeList = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
return Map.of(
"sub", authentication.getName(),
"client_id", authentication.getAttributes().get("client_id"),
"organization_id", authentication.getAttributes().get("organization_id"),
"scopes", scopeList,
"audience", authentication.getAttributes().get("aud")
);
}
@Get("/protected/detailed")
public Map<String, Object> detailedEndpoint(Authentication authentication) {
// Tu lógica del endpoint protegido
return Map.of(
"sub", authentication.getName(),
"client_id", authentication.getAttributes().get("client_id"),
"organization_id", authentication.getAttributes().get("organization_id"),
"scopes", authentication.getAttributes().get("scope"),
"audience", authentication.getAttributes().get("aud"),
"message", "Datos protegidos accedidos correctamente"
);
}
}
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
public class MainVerticle extends AbstractVerticle {
@Override
public void start(Promise<Void> startPromise) throws Exception {
Router router = Router.router(vertx);
// Aplica middleware a las rutas protegidas
router.route("/api/protected*").handler(new JwtAuthHandler(vertx));
router.get("/api/protected").handler(this::protectedEndpoint);
router.get("/api/protected/detailed").handler(this::detailedEndpoint);
vertx.createHttpServer()
.requestHandler(router)
.listen(8080, result -> {
if (result.succeeded()) {
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
});
}
private void protectedEndpoint(RoutingContext context) {
// Accede al principal JWT directamente desde el contexto
JsonObject principal = context.get("auth");
if (principal == null) {
context.response()
.setStatusCode(500)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"JWT principal not found\"}");
return;
}
String scopes = principal.getString("scope");
JsonObject response = new JsonObject()
.put("sub", principal.getString("sub"))
.put("client_id", principal.getString("client_id"))
.put("organization_id", principal.getString("organization_id"))
.put("scopes", scopes != null ? scopes.split(" ") : new String[0])
.put("audience", principal.getJsonArray("aud"));
context.response()
.putHeader("Content-Type", "application/json")
.end(response.encode());
}
private void detailedEndpoint(RoutingContext context) {
// Tu lógica del endpoint protegido
JsonObject principal = context.get("auth");
JsonObject response = new JsonObject()
.put("sub", principal.getString("sub"))
.put("client_id", principal.getString("client_id"))
.put("organization_id", principal.getString("organization_id"))
.put("scopes", principal.getString("scope"))
.put("audience", principal.getJsonArray("aud"))
.put("message", "Datos protegidos accedidos correctamente");
context.response()
.putHeader("Content-Type", "application/json")
.end(response.encode());
}
}
Ya hemos configurado el middleware de autenticación (Authentication) y autorización (Authorization) en las secciones anteriores. Ahora podemos crear un controlador protegido que valide los tokens de acceso (Access tokens) y extraiga los reclamos (Claims) de las solicitudes autenticadas.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace YourApiNamespace.Controllers
{
[ApiController]
[Route("api/[controller]")]
[Authorize] // Requiere autenticación para todas las acciones en este controlador
public class ProtectedController : ControllerBase
{
[HttpGet]
public IActionResult GetProtectedData()
{
// Información del token de acceso directamente desde los reclamos del usuario
var sub = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? User.FindFirst("sub")?.Value;
var clientId = User.FindFirst("client_id")?.Value;
var organizationId = User.FindFirst("organization_id")?.Value;
var scopes = User.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
var audience = User.FindAll("aud").Select(c => c.Value).ToArray();
return Ok(new {
sub,
client_id = clientId,
organization_id = organizationId,
scopes,
audience
});
}
[HttpGet("claims")]
public IActionResult GetAllClaims()
{
// Devuelve todos los reclamos para depuración / inspección
var claims = User.Claims.Select(c => new { c.Type, c.Value }).ToList();
return Ok(new { claims });
}
}
}
- Laravel
- Symfony
- Slim
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::middleware('auth.token')->group(function () {
Route::get('/api/protected', function (Request $request) {
// Accede a la información de autenticación desde los atributos de la solicitud
$auth = $request->attributes->get('auth');
return ['auth' => $auth->toArray()];
});
Route::get('/api/protected/detailed', function (Request $request) {
// Tu lógica para el endpoint protegido
$auth = $request->attributes->get('auth');
return [
'auth' => $auth->toArray(),
'message' => 'Datos protegidos accedidos correctamente'
];
});
});
O usando controladores:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ProtectedController extends Controller
{
public function __construct()
{
$this->middleware('auth.token');
}
public function index(Request $request)
{
// Accede a la información de autenticación desde los atributos de la solicitud
$auth = $request->attributes->get('auth');
return ['auth' => $auth->toArray()];
}
public function show(Request $request)
{
// Tu lógica para el endpoint protegido
$auth = $request->attributes->get('auth');
return [
'auth' => $auth->toArray(),
'message' => 'Datos protegidos accedidos correctamente'
];
}
}
<?php
namespace App\Controller\Api;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/api/protected')]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class ProtectedController extends AbstractController
{
#[Route('', methods: ['GET'])]
public function index(Request $request): JsonResponse
{
// Accede a la información de autenticación desde los atributos de la solicitud
$auth = $request->attributes->get('auth');
return $this->json(['auth' => $auth->toArray()]);
}
#[Route('/detailed', methods: ['GET'])]
public function detailed(Request $request): JsonResponse
{
// Tu lógica para el endpoint protegido
$auth = $request->attributes->get('auth');
return $this->json([
'auth' => $auth->toArray(),
'message' => 'Datos protegidos accedidos correctamente'
]);
}
}
<?php
use App\Middleware\JwtMiddleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
// Aplica el middleware a las rutas protegidas
$app->group('/api/protected', function ($group) {
$group->get('', function (Request $request, Response $response) {
// Accede a la información de autenticación desde los atributos de la solicitud
$auth = $request->getAttribute('auth');
$response->getBody()->write(json_encode(['auth' => $auth->toArray()]));
return $response->withHeader('Content-Type', 'application/json');
});
$group->get('/detailed', function (Request $request, Response $response) {
// Tu lógica para el endpoint protegido
$auth = $request->getAttribute('auth');
$data = [
'auth' => $auth->toArray(),
'message' => 'Datos protegidos accedidos correctamente'
];
$response->getBody()->write(json_encode($data));
return $response->withHeader('Content-Type', 'application/json');
});
})->add(new JwtMiddleware());
// Endpoint público (no protegido)
$app->get('/', function (Request $request, Response $response) {
$response->getBody()->write(json_encode(['message' => 'Endpoint público']));
return $response->withHeader('Content-Type', 'application/json');
});
$app->run();
O usando un enfoque más estructurado:
<?php
namespace App\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class ProtectedController
{
public function index(Request $request, Response $response): Response
{
// Accede a la información de autenticación desde los atributos de la solicitud
$auth = $request->getAttribute('auth');
$response->getBody()->write(json_encode(['auth' => $auth->toArray()]));
return $response->withHeader('Content-Type', 'application/json');
}
public function detailed(Request $request, Response $response): Response
{
// Tu lógica para el endpoint protegido
$auth = $request->getAttribute('auth');
$data = [
'auth' => $auth->toArray(),
'message' => 'Datos protegidos accedidos correctamente'
];
$response->getBody()->write(json_encode($data));
return $response->withHeader('Content-Type', 'application/json');
}
}
- Rails
- Sinatra
- Grape
class ApplicationController < ActionController::API # Solo para apps API
# class ApplicationController < ActionController::Base # Para apps Rails completas
include JwtAuthentication
end
class Api::ProtectedController < ApplicationController
before_action :verify_access_token
def index
# Accede a la información de autenticación desde @auth
render json: { auth: @auth.to_h }
end
def show
# Tu lógica para el endpoint protegido
render json: {
auth: @auth.to_h,
message: "Datos protegidos accedidos correctamente"
}
end
end
Rails.application.routes.draw do
namespace :api do
resources :protected, only: [:index, :show]
end
end
require 'sinatra'
require 'json'
require_relative 'auth_middleware'
require_relative 'auth_constants'
require_relative 'auth_info'
require_relative 'authorization_error'
require_relative 'auth_helpers'
require_relative 'jwt_validator'
# Aplica el middleware
use AuthMiddleware
get '/api/protected' do
content_type :json
# Accede a la información de autenticación desde env
auth = env['auth']
{ auth: auth.to_h }.to_json
end
get '/api/protected/:id' do
content_type :json
# Tu lógica para el endpoint protegido
auth = env['auth']
{
auth: auth.to_h,
id: params[:id],
message: "Datos protegidos accedidos correctamente"
}.to_json
end
# Endpoint público (no protegido por el middleware)
get '/' do
content_type :json
{ message: "Endpoint público" }.to_json
end
require 'grape'
require_relative 'auth_helpers'
require_relative 'auth_constants'
require_relative 'auth_info'
require_relative 'authorization_error'
require_relative 'jwt_validator'
class API < Grape::API
format :json
helpers GrapeAuthHelpers
namespace :api do
namespace :protected do
before do
authenticate_user!
end
get do
# Accede a la información de autenticación desde el helper de autenticación
{ auth: auth.to_h }
end
get ':id' do
# Tu lógica para el endpoint protegido
{
auth: auth.to_h,
id: params[:id],
message: "Datos protegidos accedidos correctamente"
}
end
end
end
# Endpoint público (no protegido)
get :public do
{ message: "Endpoint público" }
end
end
require_relative 'api'
run API
- Axum
- Actix Web
- Rocket
use axum::{
extract::Extension,
http::StatusCode,
middleware,
response::Json,
routing::get,
Router,
};
use serde_json::{json, Value};
use std::sync::Arc;
use tower_http::cors::CorsLayer;
mod lib;
mod jwt_validator;
mod middleware as jwt_middleware;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
#[tokio::main]
async fn main() {
let validator = Arc::new(JwtValidator::new().await.expect("Failed to initialize JWT validator"));
let app = Router::new()
.route("/api/protected", get(protected_handler))
.route("/api/protected/detailed", get(detailed_handler))
.layer(middleware::from_fn(jwt_middleware::jwt_middleware))
.layer(Extension(validator))
.layer(CorsLayer::permissive());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn protected_handler(Extension(auth): Extension<AuthInfo>) -> Json<Value> {
// Accede a la información de autenticación directamente desde Extension
Json(json!({ "auth": auth }))
}
async fn detailed_handler(Extension(auth): Extension<AuthInfo>) -> Json<Value> {
// Tu lógica de endpoint protegido
Json(json!({
"auth": auth,
"message": "Datos protegidos accedidos correctamente"
}))
}
use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Result};
use serde_json::{json, Value};
use std::sync::Arc;
mod lib;
mod jwt_validator;
mod middleware as jwt_middleware;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
use jwt_middleware::JwtMiddleware;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let validator = Arc::new(JwtValidator::new().await.expect("Failed to initialize JWT validator"));
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(validator.clone()))
.wrap(Logger::default())
.service(
web::scope("/api/protected")
.wrap(JwtMiddleware::new(validator.clone()))
.route("", web::get().to(protected_handler))
.route("/detailed", web::get().to(detailed_handler))
)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
async fn protected_handler(req: HttpRequest) -> Result<web::Json<Value>> {
// Accede a la información de autenticación desde las extensiones de la solicitud
let auth = req.extensions().get::<AuthInfo>().unwrap();
Ok(web::Json(json!({ "auth": auth })))
}
async fn detailed_handler(req: HttpRequest) -> Result<web::Json<Value>> {
// Tu lógica de endpoint protegido
let auth = req.extensions().get::<AuthInfo>().unwrap();
Ok(web::Json(json!({
"auth": auth,
"message": "Datos protegidos accedidos correctamente"
})))
}
use rocket::{get, launch, routes, serde::json::Json, State};
use serde_json::{json, Value};
mod lib;
mod jwt_validator;
mod guards;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
#[get("/api/protected")]
fn protected_handler(auth: AuthInfo) -> Json<Value> {
// Accede a la información de autenticación directamente desde el guard de la solicitud
Json(json!({ "auth": auth }))
}
#[get("/api/protected/detailed")]
fn detailed_handler(auth: AuthInfo) -> Json<Value> {
// Tu lógica de endpoint protegido
Json(json!({
"auth": auth,
"message": "Datos protegidos accedidos correctamente"
}))
}
#[launch]
async fn rocket() -> _ {
let validator = JwtValidator::new().await.expect("Failed to initialize JWT validator");
rocket::build()
.manage(validator)
.mount("/", routes![protected_handler, detailed_handler])
}
Paso 5: Prueba tu implementación
Prueba tu API con tokens válidos e inválidos para asegurar que:
- Los tokens válidos pasan y otorgan acceso.
- Devuelve
401 Unauthorized
para tokens inválidos / ausentes. Devuelve403 Forbidden
para tokens válidos que no tienen los permisos o contexto requeridos.
Recursos relacionados
Personalización de reclamos de tokens JSON Web Token (JWT)OpenID Connect Discovery
RFC 8707: Indicadores de recurso