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
- 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)
config.redis_ssl = true
config.redis_ca_cert_path = '/ruta/a/certificados'
config.redis_ca_cert_name = 'redis-ca.crt'
config.redis_tls_min_version = 'TLSv1.2'
config.redis_tls_max_version = 'TLSv1.3'
config.redis_verify_mode = 'peer'
# Opcional: Configuraciones de cache y validación
config.jwks_cache_ttl = 3600 # 1 hora
config.validation_mode = :secure # :secure o :basic
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
REDIS_CA_CERT_PATH=/ruta/a/certificados
REDIS_CA_CERT_NAME=redis-ca.crt
REDIS_TLS_MIN_VERSION=TLSv1.2
REDIS_TLS_MAX_VERSION=TLSv1.3
REDIS_VERIFY_MODE=peer
# Configuración de cache
JWKS_CACHE_TTL=3600
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_token_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
Factory Method para Configuración Simplificada (Nuevo v0.3.0)
# Crear validador con una línea
validator = JwtAuthCognito.create_cognito_validator(
region: 'us-east-1',
user_pool_id: 'us-east-1_ExamplePool',
client_id: 'your-client-id',
redis_config: {
host: 'localhost',
port: 6379,
tls: true
},
enable_user_data_retrieval: true
)
# Usar inmediatamente
result = validator.validate_token_enriched(token)
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 tokensJwtAuthCognito::TokenExpiredError: Token expiradoJwtAuthCognito::TokenRevokedError: Token revocadoJwtAuthCognito::BlacklistError: Fallas en operaciones de Redis/blacklistJwtAuthCognito::ConfigurationError: Problemas de configuraciónJwtAuthCognito::JWKSError: Errores de JWKSJwtAuthCognito::RedisConnectionError: Problemas de conexión a Redis
begin
result = validator.validate_token(token)
rescue JwtAuthCognito::TokenExpiredError => e
puts "Token expirado: #{e.}"
rescue JwtAuthCognito::BlacklistError => e
puts "Error de blacklist: #{e.}"
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 (result[:error])
end
end
@app.call(env)
end
private
def (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 ('Token faltante') unless token
result = jwt_validator.validate_access_token(token)
if result[:valid]
@current_user_id = result[:sub]
@current_username = result[:username]
else
(result[:error])
end
end
def jwt_validator
@jwt_validator ||= JwtAuthCognito::JwtValidator.new
end
def ()
render json: { error: 'No Autorizado', 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 betafull-release-rc- Release completo RCfull-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
- Desarrollo: Trabajo en feature branches
- Beta: Merge a
develop→ Tag beta → Deploy automático - RC: Release branch → Tag RC → Deploy automático
- Producción: Merge a
main→ Tag estable → Deploy manual
Para más detalles, ver: scripts/setup_rubygems_deployment.md