JWT Auth Cognito

Una gema Ruby para validar tokens JWT de AWS Cognito de forma offline con funcionalidad de blacklist basada en Redis.

Características

  • Validación JWT Offline: Valida tokens JWT de Cognito sin llamar a las APIs de AWS
  • Soporte JWKS: Recuperación automática y cache de claves públicas desde el endpoint JWKS de Cognito con compatibilidad total OpenSSL
  • Blacklist de Tokens: Gestión de revocación y blacklist de tokens basada en Redis con soporte TLS completo
  • Configuración Flexible: Soporte para modos de validación seguro (producción) y básico (desarrollo)
  • Gestión de Tokens de Usuario: Rastrear e invalidar todos los tokens de un usuario específico
  • Múltiples Tipos de Token: Soporte para access tokens e ID tokens
  • UserDataService: Recuperación de datos de usuario, permisos y organizaciones desde Redis
  • Validación Enriquecida: Validación de tokens con datos contextuales del usuario
  • Manejo Integral de Errores: Degradación elegante y manejo consistente de errores
  • Soporte TLS Avanzado: Configuración completa de TLS para Redis con certificados CA

Instalación

Agrega esta línea al Gemfile de tu aplicación:

gem 'jwt_auth_cognito'

Y luego ejecuta:

$ bundle install

O instálala directamente:

$ gem install jwt_auth_cognito

Configuración

Configura la gema en un inicializador (ej. config/initializers/jwt_auth_cognito.rb):

JwtAuthCognito.configure do |config|
  # Requerido: Configuración de AWS Cognito
  config.cognito_user_pool_id = 'us-east-1_abcdef123'
  config.cognito_region = 'us-east-1'
  config.cognito_client_id = 'tu-client-id' # Opcional, para validación de audiencia
  config.cognito_client_secret = 'tu-client-secret' # Opcional, para mayor seguridad

  # Requerido: Configuración de Redis para blacklisting
  config.redis_host = 'localhost'
  config.redis_port = 6379
  config.redis_password = 'tu-password-redis' # Opcional
  config.redis_db = 0

  # Configuración TLS para Redis (Producción - compatible con auth-service)
  config.redis_ssl = true
  config.redis_ca_cert_path = 'redis'      # AWS SSM path
  config.redis_ca_cert_name = 'ca-cert'    # AWS SSM parameter name
  config.redis_verify_mode = 'peer'        # 'peer' para validación estricta, 'none' para desarrollo

  # Opcional: Configuraciones de cache y validación
  config.jwks_cache_ttl = 3600 # 1 hora
  config.validation_mode = :secure # :secure o :basic

  # Opcional: Habilitar funcionalidades específicas
  config.enable_api_key_validation = true     # Validación de API keys
  config.enable_user_data_retrieval = true    # Enriquecimiento de datos de usuario
end

Variables de Entorno

La gema soporta configuración mediante variables de entorno:

# Configuración de Cognito
COGNITO_USER_POOL_ID=us-east-1_abcdef123
COGNITO_REGION=us-east-1
COGNITO_CLIENT_ID=tu-client-id
COGNITO_CLIENT_SECRET=tu-client-secret

# Configuración de Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=tu-password
REDIS_DB=0
REDIS_TLS=true

# Configuración TLS de Redis (compatible con auth-service)
REDIS_CA_CERT_PATH=redis        # Para AWS SSM (path del parámetro)
REDIS_CA_CERT_NAME=ca-cert      # Para AWS SSM (nombre del parámetro)
REDIS_VERIFY_MODE=peer          # 'peer' para validación estricta, 'none' para desarrollo

# Configuración de cache
JWKS_CACHE_TTL=3600

# Configuración AWS para Parameter Store (SSM)
# Nota: Si no se configuran, usa la cadena de credenciales estándar de AWS (aws configure, IAM roles, etc.)
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key        # Opcional, usa aws configure si no se proporciona
AWS_SECRET_ACCESS_KEY=your-secret-key    # Opcional, usa aws configure si no se proporciona
AWS_SESSION_TOKEN=your-session-token     # Opcional, para credenciales temporales
AWS_SSM_ENDPOINT=https://ssm.us-east-1.amazonaws.com  # Opcional, para VPC endpoints

# Habilitar funcionalidades específicas
ENABLE_API_KEY_VALIDATION=true          # Validación de API keys
ENABLE_USER_DATA_RETRIEVAL=true         # Enriquecimiento de datos de usuario

Opciones de Configuración Boolean

La gema soporta las siguientes opciones boolean para habilitar funcionalidades específicas:

  • enable_api_key_validation - Habilita la validación de API keys para control de acceso a nivel de sistema y aplicación (default: false)
  • enable_user_data_retrieval - Habilita el enriquecimiento de datos de usuario con permisos, organizaciones y aplicaciones (default: false)

Estas opciones permiten control granular sobre qué características están activas, optimizando el rendimiento habilitando solo la funcionalidad necesaria.

Configuración AWS para Development

Desarrollo Local

Para desarrollo local, la gema usa la cadena de credenciales estándar de AWS:

# Opción 1: Configurar perfil por defecto (recomendado para desarrollo)
aws configure
# Configura: access key, secret key, región, formato

# Opción 2: Usar perfil específico
aws configure --profile mi-proyecto
export AWS_PROFILE=mi-proyecto

# Opción 3: Variables de entorno específicas del proyecto
export AWS_REGION=us-east-1
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=xyz123...

Orden de Prioridad de Credenciales

  1. Variables de entorno (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
  2. Archivo de credenciales (~/.aws/credentials)
  3. Perfil AWS (AWS_PROFILE o [default])
  4. IAM roles (en EC2, ECS, Lambda, etc.)

Permisos Necesarios para SSM

Tu usuario/rol AWS necesita permisos para acceder a Parameter Store:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameter",
        "ssm:GetParameters"
      ],
      "Resource": "arn:aws:ssm:us-east-1:*:parameter/redis/*"
    }
  ]
}

Debugging de Configuración AWS

La gema incluye logging detallado para diagnosis:

📡 Getting certificate from Parameter Store: /redis/ca-cert
🌍 AWS Region: us-east-1
🔑 Credentials configured: No (using IAM role/profile)  👈 Indica uso de aws configure
✅ Certificate obtained from SSM and cached

Uso

Validación Básica de Tokens

validator = JwtAuthCognito::JwtValidator.new

# Validar cualquier token JWT
result = validator.validate_token(jwt_token)

if result[:valid]
  puts "¡Token válido!"
  puts "ID de Usuario: #{result[:sub]}"
  puts "Nombre de Usuario: #{result[:username]}"
  puts "Tipo de Token: #{result[:token_use]}"
else
  puts "Token inválido: #{result[:error]}"
end

Validar Tipos Específicos de Token

# Validar específicamente access token
result = validator.validate_access_token(jwt_token)

# Validar específicamente ID token  
result = validator.validate_id_token(jwt_token)

Validación Enriquecida con UserDataService (Nuevo v0.3.0)

# Configurar UserDataService
JwtAuthCognito.configure do |config|
  # ... configuración básica ...
  config.enable_user_data_retrieval = true
end

validator = JwtAuthCognito::JwtValidator.new
validator.initialize! # Inicializar servicios

# Validación enriquecida con datos de usuario desde Redis
result = validator.validate_enriched(jwt_token)

if result[:valid]
  puts "Token válido!"
  puts "Usuario: #{result[:sub]}"

  # Datos adicionales del usuario
  if result[:user_permissions]
    puts "Apps con permisos: #{result[:user_permissions]['permissions'].keys}"
  end

  if result[:user_organizations]&.any?
    puts "Organizaciones activas:"
    result[:user_organizations].each do |org|
      puts "  - #{org['organizationId']} (roles: #{org['roles'].join(', ')})"
    end
  end

  if result[:applications]&.any?
    puts "Aplicaciones disponibles: #{result[:applications].map { |app| app['name'] }.join(', ')}"
  end
end

Opciones Avanzadas para validate_enriched

El método validate_enriched acepta múltiples parámetros para casos de uso específicos:

# Sintaxis completa
result = validator.validate_enriched(token, api_key, options)

# 1. Solo token (caso más simple)
result = validator.validate_enriched(jwt_token)

# 2. Con API key
result = validator.validate_enriched(jwt_token, api_key)

# 3. Con opciones adicionales
result = validator.validate_enriched(jwt_token, nil, {
  force_secure: true,           # Forzar validación segura (JWKS)
  require_app_access: true      # Requerir acceso a aplicación específica
})

# 4. Con API key y opciones
result = validator.validate_enriched(jwt_token, api_key, {
  force_secure: true,
  require_app_access: true
})

# 5. Solo con opciones (sin API key)
result = validator.validate_enriched(jwt_token, nil, {
  force_secure: false,          # Usar modo básico de validación
  require_app_access: false     # No verificar acceso a aplicación
})

Parámetros disponibles:

  • token (String): JWT token a validar
  • api_key (String, opcional): API key para validación adicional
  • options (Hash, opcional):
    • force_secure: Forzar validación JWKS incluso en desarrollo
    • require_app_access: Verificar que el usuario tenga acceso a la aplicación del API key
# Ejemplo con todas las opciones
result = validator.validate_enriched(
  jwt_token,
  'api-key-64-hex-characters',
  {
    force_secure: true,
    require_app_access: true
  }
)

if result[:valid]
  puts "✅ Validación completa exitosa"
  puts "Usuario: #{result[:sub]}"
  puts "API Key: #{result[:api_key][:name]}"
  puts "Permisos: #{result[:user_permissions]}"
  puts "Apps disponibles: #{result[:applications]&.map { |app| app['appId'] }}"
else
  puts "❌ Error: #{result[:error]}"
end

Validación con API Keys

Para usar validación de API keys, habilita la funcionalidad en la configuración:

# Configurar con validación de API keys habilitada
JwtAuthCognito.configure do |config|
  # ... configuración básica ...
  config.enable_api_key_validation = true
end

validator = JwtAuthCognito::JwtValidator.new
validator.initialize!

# Validar token con API key opcional
api_key = 'api-key-64-hex-characters-1234567890abcdef1234567890abcdef12345678'
result = validator.validate(jwt_token, api_key: api_key)

if result[:valid]
  puts "✅ Token y API key válidos"
  puts "Usuario: #{result[:sub]}"

  # Información del API key
  if result[:api_key_data]
    key_data = result[:api_key_data]
    puts "API Key: #{key_data[:name]}"
    puts "Scope: #{key_data[:scope]}"
    puts "Permisos: #{key_data[:permissions].join(', ')}"
    puts "App ID: #{key_data[:app_id]}" if key_data[:app_id]
  end
else
  puts "❌ Error: #{result[:error]}"
end

Tipos de API Keys soportados:

  • System API Keys (scope: 'system') - Acceso transversal a todas las aplicaciones
  • App API Keys (scope: 'app') - Acceso restringido a una aplicación específica
  • Client API Keys (scope: 'client') - Para aplicaciones cliente

Factory Method para Configuración Simplificada (Nuevo v0.3.0)

# Crear validador con conexión Redis completa
validator = JwtAuthCognito.create_cognito_validator(
  region: 'us-east-1',
  user_pool_id: 'us-east-1_ExamplePool',
  client_id: 'your-client-id',
  redis_config: {
    # Configuración básica de Redis
    host: ENV['REDIS_HOST'] || 'localhost',
    port: ENV['REDIS_PORT']&.to_i || 6379,
    password: ENV['REDIS_PASSWORD'],
    db: ENV['REDIS_DB']&.to_i || 0,

    # Configuración TLS para conexiones seguras
    tls: ENV['REDIS_TLS'] == 'true',
    ca_cert_path: ENV['REDIS_CA_CERT_PATH'],
    ca_cert_name: ENV['REDIS_CA_CERT_NAME']
  },
  enable_api_key_validation: true,      # Habilitar validación de API keys
  enable_user_data_retrieval: true     # Habilitar enriquecimiento de datos
)

# Inicializar conexiones (incluye Redis)
validator.initialize!

# Usar inmediatamente con validación enriquecida
result = validator.validate_enriched(token)

if result[:valid]
  puts "✅ Token válido con datos enriquecidos:"
  puts "Usuario: #{result[:sub]}"
  puts "Permisos: #{result[:user_permissions]}"
  puts "Organizaciones: #{result[:user_organizations]}"
  puts "Aplicaciones: #{result[:applications]}"
else
  puts "❌ Token inválido: #{result[:error]}"
end

Manejo Mejorado de Errores (Nuevo v0.3.0)

begin
  result = validator.validate_token(token)
rescue => error
  # ErrorUtils proporciona mensajes consistentes
  error_details = JwtAuthCognito::ErrorUtils.extract_error_details(error)

  puts "Error: #{error_details[:message]}"
  puts "Código: #{error_details[:code]}" if error_details[:code]

  # Para APIs - respuesta estandarizada
  api_response = JwtAuthCognito::ErrorUtils.format_validation_error(error)
  # Retorna: { valid: false, error: "mensaje", error_code: "CODIGO" }
end

Opciones Avanzadas de Validación

# Validar con requisitos personalizados
result = validator.validate_token(jwt_token, {
  user_id: 'id-usuario-esperado',
  client_id: 'client-id-esperado',
  token_use: 'access',
  required_scopes: ['read', 'write']
})

Blacklist de Tokens

# Revocar un token específico
validator.revoke_token(jwt_token, user_id: 'usuario-123')

# Revocar todos los tokens de un usuario
validator.revoke_user_tokens('usuario-123')

# Verificar si un token está en blacklist
blacklisted = validator.validate_token(jwt_token)
# Retornará { valid: false, error: "Token has been revoked" }

Validación en Lote

tokens = [token1, token2, token3]
results = validator.validate_multiple_tokens(tokens)

results.each_with_index do |result, index|
  puts "Token #{index + 1}: #{result[:valid] ? 'Válido' : result[:error]}"
end

Métodos Utilitarios

# Extraer token del header Authorization
token = validator.extract_token_from_header(request.headers['Authorization'])

# Obtener información del token
info = validator.get_token_info(token)
puts "Usuario: #{info[:username]}"
puts "Expira en: #{info[:expires_at]}"
puts "Tiene client secret: #{info[:has_client_secret]}"

# Verificar expiración
expired = validator.is_token_expired?(token)

# Obtener tiempo hasta expiración
seconds_to_expiry = validator.get_time_to_expiry(token)

# Calcular secret hash (para operaciones de Cognito)
# Útil si necesitas hacer operaciones directas con Cognito SDK
secret_hash = validator.calculate_secret_hash('username_or_email')

Uso Directo de Servicios

# Usar servicios directamente para mayor control
blacklist_service = JwtAuthCognito::TokenBlacklistService.new
jwks_service = JwtAuthCognito::JwksService.new

# Agregar token a blacklist con TTL personalizado
blacklist_service.add_to_blacklist(token, user_id: 'usuario-123')

# Validar con JWKS
result = jwks_service.validate_token_with_jwks(token)

Modos de Validación

Modo Seguro (Producción)

  • Validación completa de firma JWKS
  • Recuperación automática y cache de claves públicas
  • Validación completa de claims
  • Recomendado para entornos de producción

Modo Básico (Desarrollo)

  • Solo validación de estructura del token
  • Sin verificación de firma
  • Validación más rápida para desarrollo/testing
  • NO recomendado para producción
# Configurar modo de validación
JwtAuthCognito.configure do |config|
  config.validation_mode = :basic # Para desarrollo
  config.validation_mode = :secure # Para producción
end

Manejo de Errores

La gema proporciona tipos de error específicos:

  • JwtAuthCognito::ValidationError: Fallas de validación de tokens
  • JwtAuthCognito::TokenExpiredError: Token expirado
  • JwtAuthCognito::TokenRevokedError: Token revocado
  • JwtAuthCognito::BlacklistError: Fallas en operaciones de Redis/blacklist
  • JwtAuthCognito::ConfigurationError: Problemas de configuración
  • JwtAuthCognito::JWKSError: Errores de JWKS
  • JwtAuthCognito::RedisConnectionError: Problemas de conexión a Redis
begin
  result = validator.validate_token(token)
rescue JwtAuthCognito::TokenExpiredError => e
  puts "Token expirado: #{e.message}"
rescue JwtAuthCognito::BlacklistError => e
  puts "Error de blacklist: #{e.message}"
end

📋 Estructura del Token Decodificado

Después de una validación exitosa, el hash de resultado contiene los claims del JWT con la siguiente estructura:

Claims Principales

# Resultado de validación
{
  # ============ Claims Obligatorios JWT Standard (RFC 7519) ============

  # Subject - Identificador único del usuario (UUID)
  sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",

  # Audience - Cliente/aplicación para la cual el token fue emitido
  aud: "1234567890abcdefghijklmnop",

  # Issuer - URL del User Pool de Cognito que emitió el token
  iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX",

  # Expiration Time - Timestamp Unix (segundos) de expiración
  exp: 1735689600,  # Equivale a 2025-01-01 00:00:00 UTC

  # Issued At - Timestamp Unix (segundos) de emisión
  iat: 1735603200,  # Equivale a 2024-12-31 00:00:00 UTC

  # Token Use - Tipo de token Cognito
  token_use: "access",  # 'access' para Access Tokens, 'id' para ID Tokens

  # ============ Claims Opcionales de Usuario ============

  # Email del usuario (opcional)
  email: "usuario@ejemplo.com",

  # Verificación de email (opcional)
  email_verified: true,

  # Número de teléfono (opcional)
  phone_number: "+12025551234",

  # Verificación de teléfono (opcional)
  phone_number_verified: true,

  # Nombre de usuario (opcional)
  username: "john.doe",

  # ============ Claims Específicos de AWS Cognito ============

  # Username en formato Cognito (opcional)
  "cognito:username": "john.doe",

  # Grupos de Cognito (opcional)
  "cognito:groups": ["admin", "users"],

  # Scope OAuth2 - Solo en Access Tokens
  scope: "openid email profile",

  # Authentication Time - Timestamp Unix (opcional)
  auth_time: 1735603200,

  # JWT ID - Identificador único del token (opcional)
  jti: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",

  # ============ Custom Attributes ============
  # Cualquier atributo custom definido en Cognito
  "custom:tenant_id": "company-123"
}

Ejemplos Reales de Tokens Decodificados

Access Token (token_use: "access")

result = validator.validate_access_token(token)

# Resultado:
{
  valid: true,
  sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX",
  client_id: "1234567890abcdefghijklmnop",
  aud: "1234567890abcdefghijklmnop",
  token_use: "access",
  scope: "openid email profile",
  auth_time: 1735603200,
  exp: 1735689600,
  iat: 1735603200,
  jti: "xyz-789-def-456",
  username: "john.doe",
  "cognito:username": "john.doe",
  "cognito:groups": ["admin", "users"]
}

ID Token (token_use: "id")

result = validator.validate_id_token(token)

# Resultado:
{
  valid: true,
  sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX",
  aud: "1234567890abcdefghijklmnop",
  token_use: "id",
  auth_time: 1735603200,
  exp: 1735689600,
  iat: 1735603200,
  email: "john.doe@example.com",
  email_verified: true,
  phone_number: "+12025551234",
  phone_number_verified: true,
  "cognito:username": "john.doe",
  "cognito:groups": ["admin"],
  "custom:department": "engineering",
  "custom:employee_id": "EMP-12345"
}

Uso del Token Decodificado en tu Aplicación

result = validator.validate_token(token)

if result[:valid]
  # ✅ Identificación del usuario
  puts "User ID: #{result[:sub]}"
  puts "Username: #{result['cognito:username'] || result[:username]}"

  # ✅ Información de contacto
  if result[:email]
    puts "Email: #{result[:email]}"
    puts "Email verificado: #{result[:email_verified]}"
  end

  # ✅ Autorización basada en grupos
  if result['cognito:groups']&.include?('admin')
    puts "Usuario es administrador"
  end

  # ✅ Validación de tiempo
  expires_at = Time.at(result[:exp])
  puts "Token expira: #{expires_at}"

  issued_at = Time.at(result[:iat])
  puts "Token emitido: #{issued_at}"

  # ✅ Atributos custom
  if result['custom:tenant_id']
    puts "Tenant ID: #{result['custom:tenant_id']}"
  end

  # ✅ Scope OAuth2 (solo Access Tokens)
  if result[:token_use] == 'access' && result[:scope]
    scopes = result[:scope].split(' ')
    puts "Permisos OAuth2: #{scopes}"
  end
end

Diferencias entre Access Token e ID Token

Campo Access Token ID Token
token_use "access" "id"
scope ✅ Incluido ❌ No incluido
client_id ✅ Incluido ❌ No incluido
email ❌ No incluido ✅ Incluido
email_verified ❌ No incluido ✅ Incluido
phone_number ❌ No incluido ✅ Incluido
Custom Attributes ❌ No incluido ✅ Incluido
Uso Principal Autorización en APIs Información del usuario

Notas Importantes

  • Claims opcionales: La disponibilidad de campos como email, phone_number, y custom:* depende de tu configuración de Cognito
  • Token Use: Usa result[:token_use] para determinar el tipo de token y qué campos esperar
  • Timestamps: Los campos exp, iat, y auth_time están en formato Unix timestamp (segundos desde 1970-01-01)
  • Custom Attributes: Los atributos custom de Cognito tienen el prefijo custom: en sus nombres
  • Grupos Cognito: Los grupos se almacenan en el array cognito:groups cuando están configurados

📦 Estructura de la Respuesta de Validación

Respuesta Básica de Validación

result = validator.validate_token(token)

# Estructura del resultado:
{
  valid: true,          # Boolean - Indica si el token es válido
  sub: "user-id",       # String - ID del usuario
  username: "john.doe", # String - Nombre de usuario
  token_use: "access",  # String - Tipo de token
  # ... más claims del token ...
  error: nil            # String - Mensaje de error (solo si valid: false)
}

Respuesta con API Key

Cuando se valida con un API Key, el resultado incluye información adicional:

result = validator.validate(token, api_key: api_key)

# Resultado completo:
{
  valid: true,
  sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  username: "john.doe",
  token_use: "access",
  # ... otros claims del token ...

  # Información del API Key
  api_key_data: {
    name: "production-api-key",
    permissions: ["auth:access", "users:read"],
    app_id: "my-application",
    scope: "client",
    created_at: 1735603200000,
    last_used: 1735689600000,
    is_active: true,
    metadata: {
      created_for: "Integration Team",
      description: "API Key for production integration",
      environment: "production"
    }
  }
}

Estructura de api_key_data

{
  # Nombre identificador del API Key
  name: "production-api-key",

  # Lista de permisos asignados
  permissions: ["auth:access", "users:read"],

  # ID de la aplicación asociada (opcional para scope 'system')
  app_id: "my-application",

  # Alcance del API Key
  scope: "client",  # Valores: 'app', 'system', 'client'

  # Timestamp de creación (milisegundos)
  created_at: 1735603200000,

  # Timestamp del último uso (nil si nunca usado)
  last_used: 1735689600000,

  # Estado del API Key
  is_active: true,

  # Metadatos personalizados
  metadata: {
    created_for: "Integration Team",
    environment: "production"
  }
}

Diferencias entre Scopes de API Keys

Scope Descripción Restricciones Uso Típico
system Acceso transversal a todas las aplicaciones Ninguna - acceso completo Administración, integraciones de sistema
app Acceso limitado a una aplicación específica Solo puede acceder al app_id asociado Integraciones de aplicaciones específicas
client Acceso de cliente/frontend Restricciones según permisos asignados Aplicaciones frontend, móviles

Ejemplos de Uso con API Key

1. Validación Básica con API Key

result = validator.validate(token, api_key: api_key)

if result[:valid]
  puts "✅ Token válido"
  puts "Usuario: #{result[:username]}"

  # Información del API Key
  if result[:api_key_data]
    key_data = result[:api_key_data]
    puts "API Key: #{key_data[:name]}"
    puts "Scope: #{key_data[:scope]}"
    puts "Permisos: #{key_data[:permissions].join(', ')}"
    puts "App ID: #{key_data[:app_id]}" if key_data[:app_id]
  end
else
  puts "❌ Token inválido: #{result[:error]}"
end

2. Autorización basada en API Key Scope

result = validator.validate(token, api_key: api_key)

if result[:valid] && result[:api_key_data]
  key_data = result[:api_key_data]

  # Verificar si tiene acceso system (transversal)
  if key_data[:scope] == 'system'
    puts "✅ Acceso system - puede acceder a todas las apps"
  end

  # Verificar si tiene acceso a app específica
  if key_data[:scope] == 'app' && key_data[:app_id] == 'my-target-app'
    puts "✅ Acceso autorizado a my-target-app"
  end

  # Verificar permisos específicos
  if key_data[:permissions].include?('users:write')
    puts "✅ Puede modificar usuarios"
  end
end

3. Usar Metadata del API Key

result = validator.validate(token, api_key: api_key)

if result[:valid] && result[:api_key_data]
   = result[:api_key_data][:metadata]

  # Acceder a información personalizada
  puts "Creado para: #{[:created_for]}"
  puts "Descripción: #{[:description]}"
  puts "Ambiente: #{[:environment]}"

  # Control de acceso basado en ambiente
  if [:environment] == 'production'
    Rails.logger.info "⚠️ API Key de producción - logging extra habilitado"
  end
end

4. Tracking de Último Uso

result = validator.validate(token, api_key: api_key)

if result[:valid] && result[:api_key_data]
  key_data = result[:api_key_data]

  # Verificar última vez usado
  if key_data[:last_used]
    last_used_date = Time.at(key_data[:last_used] / 1000)
    puts "Última vez usado: #{last_used_date}"

    # Detectar API Keys inactivos (más de 30 días sin uso)
    days_since_last_use = (Time.now - last_used_date) / (60 * 60 * 24)
    if days_since_last_use > 30
      puts "⚠️ API Key inactivo por más de 30 días"
    end
  else
    puts "ℹ️ API Key nunca usado antes"
  end

  # Antigüedad del API Key
  created_date = Time.at(key_data[:created_at] / 1000)
  puts "Creado el: #{created_date}"
end

Respuesta con Datos Enriquecidos

Cuando usas validate_enriched(), obtienes campos adicionales:

result = validator.validate_enriched(token, api_key)

# Resultado completo con datos enriquecidos:
{
  valid: true,
  sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  username: "john.doe",
  email: "john.doe@example.com",
  # ... otros campos del token ...

  api_key_data: {
    name: "production-api-key",
    permissions: ["auth:access", "users:read"],
    app_id: "my-application",
    scope: "client",
    created_at: 1735603200000,
    last_used: 1735689600000,
    is_active: true,
    metadata: { environment: "production" }
  },

  user_permissions: {
    "userId" => "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "permissions" => {
      "my-app" => {
        "org-123" => {
          "roles" => ["admin", "user"],
          "effectivePermissions" => ["users:read", "users:write"],
          "status" => "active"
        }
      }
    }
  },

  user_organizations: [
    {
      "appId" => "my-app",
      "organizationId" => "org-123",
      "roles" => ["admin"],
      "status" => "active",
      "effectivePermissions" => ["users:read", "users:write"]
    }
  ],

  applications: [
    {
      "appId" => "my-app",
      "name" => "My Application",
      "isActive" => true,
      "createdAt" => 1735603200000,
      "updatedAt" => 1735689600000
    }
  ]
}

Manejo de Errores

Cuando la validación falla, obtienes un mensaje de error claro:

result = validator.validate_token(token)

unless result[:valid]
  puts "❌ Error: #{result[:error]}"

  # Ejemplos de errores comunes:
  # - "Token has expired"
  # - "Invalid token signature"
  # - "Invalid audience"
  # - "Token has been revoked"
  # - "API key is inactive"
  # - "No access to application"
end

Integración con Rails

Ejemplo de Middleware

class JwtAuthenticationMiddleware
  def initialize(app)
    @app = app
    @validator = JwtAuthCognito::JwtValidator.new
  end

  def call(env)
    request = Rack::Request.new(env)
    token = @validator.extract_token_from_header(request.get_header('HTTP_AUTHORIZATION'))

    if token
      result = @validator.validate_access_token(token)
      if result[:valid]
        env['current_user_id'] = result[:sub]
        env['current_username'] = result[:username]
      else
        return unauthorized_response(result[:error])
      end
    end

    @app.call(env)
  end

  private

  def unauthorized_response(error)
    [401, { 'Content-Type' => 'application/json' }, 
     [{ error: 'No Autorizado', message: error }.to_json]]
  end
end

Helper para Controladores

module JwtAuthHelper
  extend ActiveSupport::Concern

  included do
    before_action :authenticate_jwt_token!
  end

  private

  def authenticate_jwt_token!
    token = jwt_validator.extract_token_from_header(request.headers['Authorization'])
    return render_unauthorized('Token faltante') unless token

    result = jwt_validator.validate_access_token(token)

    if result[:valid]
      @current_user_id = result[:sub]
      @current_username = result[:username]
    else
      render_unauthorized(result[:error])
    end
  end

  def jwt_validator
    @jwt_validator ||= JwtAuthCognito::JwtValidator.new
  end

  def render_unauthorized(message)
    render json: { error: 'No Autorizado', message: message }, status: :unauthorized
  end
end

Integración con Llegando-Neo

# En config/initializers/jwt_auth_cognito.rb
JwtAuthCognito.configure do |config|
  # Reutilizar configuración existente de Redis
  redis_config = Rails.application.config_for(:redis)
  config.redis_host = redis_config[:host]
  config.redis_port = redis_config[:port]
  config.redis_password = redis_config[:password]
  config.redis_ssl = redis_config[:ssl]

  # Configuración de Cognito desde secrets
  config.cognito_user_pool_id = Rails.application.secrets.cognito_user_pool_id
  config.cognito_region = Rails.application.secrets.cognito_region
  config.cognito_client_id = Rails.application.secrets.cognito_client_id

  config.validation_mode = Rails.env.production? ? :secure : :basic
end

Compatibilidad

  • Ruby: >= 2.7.0 (Compatible con llegando-neo Ruby 2.7.5)
  • Rails: >= 5.0 (Compatible con llegando-neo Rails 5.2.6)
  • Redis: >= 4.2.5 (Compatible con llegando-neo redis >= 4.2.5)

Desarrollo

Después de clonar el repositorio, ejecuta bin/setup para instalar dependencias. Luego, ejecuta rake spec para correr las pruebas. También puedes ejecutar bin/console para una consola interactiva que te permitirá experimentar.

Para instalar esta gema localmente, ejecuta bundle exec rake install. Para liberar una nueva versión, actualiza el número de versión en version.rb, y luego ejecuta bundle exec rake release.

Contribución

Los reportes de bugs y pull requests son bienvenidos en GitHub.

Licencia

La gema está disponible como código abierto bajo los términos de la Licencia MIT.

Generación Automática de Configuración

Usando el generador de Rails

# Generar configuración automáticamente
rails generate jwt_auth_cognito:install

Usando tareas Rake

# Instalar configuración
rake jwt_auth_cognito:install

# Ver configuración actual
rake jwt_auth_cognito:config

# Probar conexiones
rake jwt_auth_cognito:test_cognito
rake jwt_auth_cognito:test_redis

# Limpiar blacklist
rake jwt_auth_cognito:clear_blacklist

Esto generará automáticamente:

  • config/initializers/jwt_auth_cognito.rb - Archivo de configuración
  • .env.example - Variables de entorno de ejemplo
  • Configuración optimizada para tu proyecto Rails

Deployment y CI/CD

Configuración de Deployment Automático

Este gem utiliza Bitbucket Pipelines para deployment automático a RubyGems.org:

1. Configurar Token de RubyGems

# Obtener instrucciones para el token
ruby scripts/generate_rubygems_token.rb

# Probar configuración local (opcional)
export RUBYGEMS_API_KEY='tu_token_aqui'
ruby scripts/test_rubygems_token.rb

2. Variables de Bitbucket

En tu repositorio de Bitbucket:

  • Settings → Repository variables
  • Añadir variable: RUBYGEMS_API_KEY (marcada como secured)

3. Comandos de Release

# Release Beta
git tag v0.3.0-beta.1
git push origin v0.3.0-beta.1

# Release RC  
git tag v0.3.0-rc.1
git push origin v0.3.0-rc.1

# Release Estable (requiere confirmación manual)
git tag v0.3.0
git push origin v0.3.0

4. Pipelines Manuales

En Bitbucket Pipelines → Run custom pipeline:

  • full-release-beta - Release completo beta
  • full-release-rc - Release completo RC
  • full-release-stable - Release completo estable (requiere confirmación)
  • test-build - Solo testing del build

5. Helper de Deployment

# Ver estado y comandos disponibles
ruby scripts/deployment_helper.rb

# Ver comandos específicos
ruby scripts/deployment_helper.rb commands

# Ver configuración necesaria  
ruby scripts/deployment_helper.rb setup

Flujo de Trabajo Recomendado

  1. Desarrollo: Trabajo en feature branches
  2. Beta: Merge a develop → Tag beta → Deploy automático
  3. RC: Release branch → Tag RC → Deploy automático
  4. Producción: Merge a main → Tag estable → Deploy manual

Para más detalles, ver: scripts/setup_rubygems_deployment.md