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