jwt_auth_cognito 1.0.0.pre.beta.9 → 1.0.0.pre.beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/CLAUDE.md +1 -1
- data/README.md +453 -0
- data/lib/jwt_auth_cognito/api_key_validator.rb +1 -1
- data/lib/jwt_auth_cognito/authorization_concern.rb +118 -0
- data/lib/jwt_auth_cognito/jwks_service.rb +9 -2
- data/lib/jwt_auth_cognito/jwt_validator.rb +46 -0
- data/lib/jwt_auth_cognito/permission_checker.rb +36 -0
- data/lib/jwt_auth_cognito/user_data_service.rb +62 -0
- data/lib/jwt_auth_cognito/version.rb +1 -1
- data/lib/jwt_auth_cognito.rb +5 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8069ccdfaed845402cd9053a2db66b2bc74359649de17ca2ce7ad010a939722
|
|
4
|
+
data.tar.gz: 3ba1441f8016d8e2d5b1bca7913fd7700f0ae51c8470ab1e6f06a2e1a9640697
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 897a6f309d17ba4f312e5b9b86a8daf4ab76050a6b5cd17543eaaf728c3ba242bc1ec34a8efb25f17395095cccf5225bf918850fb4f373ba0132311c3c0e84c7
|
|
7
|
+
data.tar.gz: 785494e60e28e09a5b3343eaf9b2dfc0d9a70343cf8f56ddd81768b172c0753696a3b03df84f24510ba50687d9d7cb6af8a3bce72fad2331e88bc195d9dc73b5
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.0-beta.11] - 2025-01-23
|
|
11
|
+
|
|
12
|
+
### Improved
|
|
13
|
+
|
|
14
|
+
- **Documentation Enhancement**: Comprehensive documentation of token structure and validation responses
|
|
15
|
+
- Added detailed structure documentation for decoded token hash with all JWT claims
|
|
16
|
+
- Documented complete validation result structure including `api_key_data` field
|
|
17
|
+
- Added comprehensive `api_key_data` structure documentation with all fields explained
|
|
18
|
+
- Included real-world examples of Access Token and ID Token decoded structures in Ruby format
|
|
19
|
+
- Added comparison table showing differences between Access and ID tokens
|
|
20
|
+
- Documented API Key scopes (`system`, `app`, `client`) with usage patterns
|
|
21
|
+
- Added 4 practical examples: basic validation, scope-based authorization, metadata usage, and usage tracking
|
|
22
|
+
- Included documentation for enriched validation results with user data fields
|
|
23
|
+
- Enhanced error handling documentation with common error messages
|
|
24
|
+
|
|
25
|
+
### Documentation
|
|
26
|
+
|
|
27
|
+
- **Token Structure**: Complete guide to JWT claims (standard, optional, and Cognito-specific)
|
|
28
|
+
- **Response Structure**: Detailed documentation of validation response hash format
|
|
29
|
+
- **API Key Details**: Full explanation of API key data including permissions, scope, and metadata
|
|
30
|
+
- **Practical Examples**: Real-world Ruby code examples for common use cases
|
|
31
|
+
- **Ruby Idioms**: Documentation adapted to Ruby conventions and patterns
|
|
32
|
+
|
|
33
|
+
## [1.0.0-beta.10] - 2025-01-23
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **Audience Validation**: Fixed overly strict audience validation for AWS Cognito access tokens
|
|
38
|
+
- Access tokens from Cognito typically don't include 'aud' claim, only ID tokens do
|
|
39
|
+
- Modified JWKS validation to only enforce audience checking for ID tokens (`token_use: 'id'`)
|
|
40
|
+
- Access tokens (`token_use: 'access'`) now skip audience validation as per AWS Cognito standards
|
|
41
|
+
- Resolves "Invalid audience. Expected [client_id], received <none>" error for access tokens
|
|
42
|
+
- Maintains proper security validation for ID tokens
|
|
43
|
+
|
|
44
|
+
### Improved
|
|
45
|
+
|
|
46
|
+
- **JWT Standards Compliance**: Enhanced compatibility with AWS Cognito token specifications
|
|
47
|
+
- Pre-decodes tokens to determine type before applying validation rules
|
|
48
|
+
- Follows AWS Cognito best practices for token type-specific validation
|
|
49
|
+
- Maintains backward compatibility with existing ID token validation
|
|
50
|
+
|
|
10
51
|
## [1.0.0-beta.9] - 2025-01-22
|
|
11
52
|
|
|
12
53
|
### Fixed
|
data/CLAUDE.md
CHANGED
|
@@ -304,7 +304,7 @@ JWKS_CACHE_TTL=3600 # 1 hour
|
|
|
304
304
|
|
|
305
305
|
## Version Compatibility
|
|
306
306
|
|
|
307
|
-
### ✅ **Updated January 2025 - Version 1.0.0-beta.
|
|
307
|
+
### ✅ **Updated January 2025 - Version 1.0.0-beta.10**
|
|
308
308
|
|
|
309
309
|
**Stable production-ready beta with complete pipeline compatibility**
|
|
310
310
|
|
data/README.md
CHANGED
|
@@ -519,6 +519,459 @@ rescue JwtAuthCognito::BlacklistError => e
|
|
|
519
519
|
end
|
|
520
520
|
```
|
|
521
521
|
|
|
522
|
+
## 📋 Estructura del Token Decodificado
|
|
523
|
+
|
|
524
|
+
Después de una validación exitosa, el hash de resultado contiene los claims del JWT con la siguiente estructura:
|
|
525
|
+
|
|
526
|
+
### Claims Principales
|
|
527
|
+
|
|
528
|
+
```ruby
|
|
529
|
+
# Resultado de validación
|
|
530
|
+
{
|
|
531
|
+
# ============ Claims Obligatorios JWT Standard (RFC 7519) ============
|
|
532
|
+
|
|
533
|
+
# Subject - Identificador único del usuario (UUID)
|
|
534
|
+
sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
535
|
+
|
|
536
|
+
# Audience - Cliente/aplicación para la cual el token fue emitido
|
|
537
|
+
aud: "1234567890abcdefghijklmnop",
|
|
538
|
+
|
|
539
|
+
# Issuer - URL del User Pool de Cognito que emitió el token
|
|
540
|
+
iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX",
|
|
541
|
+
|
|
542
|
+
# Expiration Time - Timestamp Unix (segundos) de expiración
|
|
543
|
+
exp: 1735689600, # Equivale a 2025-01-01 00:00:00 UTC
|
|
544
|
+
|
|
545
|
+
# Issued At - Timestamp Unix (segundos) de emisión
|
|
546
|
+
iat: 1735603200, # Equivale a 2024-12-31 00:00:00 UTC
|
|
547
|
+
|
|
548
|
+
# Token Use - Tipo de token Cognito
|
|
549
|
+
token_use: "access", # 'access' para Access Tokens, 'id' para ID Tokens
|
|
550
|
+
|
|
551
|
+
# ============ Claims Opcionales de Usuario ============
|
|
552
|
+
|
|
553
|
+
# Email del usuario (opcional)
|
|
554
|
+
email: "usuario@ejemplo.com",
|
|
555
|
+
|
|
556
|
+
# Verificación de email (opcional)
|
|
557
|
+
email_verified: true,
|
|
558
|
+
|
|
559
|
+
# Número de teléfono (opcional)
|
|
560
|
+
phone_number: "+12025551234",
|
|
561
|
+
|
|
562
|
+
# Verificación de teléfono (opcional)
|
|
563
|
+
phone_number_verified: true,
|
|
564
|
+
|
|
565
|
+
# Nombre de usuario (opcional)
|
|
566
|
+
username: "john.doe",
|
|
567
|
+
|
|
568
|
+
# ============ Claims Específicos de AWS Cognito ============
|
|
569
|
+
|
|
570
|
+
# Username en formato Cognito (opcional)
|
|
571
|
+
"cognito:username": "john.doe",
|
|
572
|
+
|
|
573
|
+
# Grupos de Cognito (opcional)
|
|
574
|
+
"cognito:groups": ["admin", "users"],
|
|
575
|
+
|
|
576
|
+
# Scope OAuth2 - Solo en Access Tokens
|
|
577
|
+
scope: "openid email profile",
|
|
578
|
+
|
|
579
|
+
# Authentication Time - Timestamp Unix (opcional)
|
|
580
|
+
auth_time: 1735603200,
|
|
581
|
+
|
|
582
|
+
# JWT ID - Identificador único del token (opcional)
|
|
583
|
+
jti: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
584
|
+
|
|
585
|
+
# ============ Custom Attributes ============
|
|
586
|
+
# Cualquier atributo custom definido en Cognito
|
|
587
|
+
"custom:tenant_id": "company-123"
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Ejemplos Reales de Tokens Decodificados
|
|
592
|
+
|
|
593
|
+
#### Access Token (token_use: "access")
|
|
594
|
+
|
|
595
|
+
```ruby
|
|
596
|
+
result = validator.validate_access_token(token)
|
|
597
|
+
|
|
598
|
+
# Resultado:
|
|
599
|
+
{
|
|
600
|
+
valid: true,
|
|
601
|
+
sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
602
|
+
iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX",
|
|
603
|
+
client_id: "1234567890abcdefghijklmnop",
|
|
604
|
+
aud: "1234567890abcdefghijklmnop",
|
|
605
|
+
token_use: "access",
|
|
606
|
+
scope: "openid email profile",
|
|
607
|
+
auth_time: 1735603200,
|
|
608
|
+
exp: 1735689600,
|
|
609
|
+
iat: 1735603200,
|
|
610
|
+
jti: "xyz-789-def-456",
|
|
611
|
+
username: "john.doe",
|
|
612
|
+
"cognito:username": "john.doe",
|
|
613
|
+
"cognito:groups": ["admin", "users"]
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
#### ID Token (token_use: "id")
|
|
618
|
+
|
|
619
|
+
```ruby
|
|
620
|
+
result = validator.validate_id_token(token)
|
|
621
|
+
|
|
622
|
+
# Resultado:
|
|
623
|
+
{
|
|
624
|
+
valid: true,
|
|
625
|
+
sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
626
|
+
iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX",
|
|
627
|
+
aud: "1234567890abcdefghijklmnop",
|
|
628
|
+
token_use: "id",
|
|
629
|
+
auth_time: 1735603200,
|
|
630
|
+
exp: 1735689600,
|
|
631
|
+
iat: 1735603200,
|
|
632
|
+
email: "john.doe@example.com",
|
|
633
|
+
email_verified: true,
|
|
634
|
+
phone_number: "+12025551234",
|
|
635
|
+
phone_number_verified: true,
|
|
636
|
+
"cognito:username": "john.doe",
|
|
637
|
+
"cognito:groups": ["admin"],
|
|
638
|
+
"custom:department": "engineering",
|
|
639
|
+
"custom:employee_id": "EMP-12345"
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Uso del Token Decodificado en tu Aplicación
|
|
644
|
+
|
|
645
|
+
```ruby
|
|
646
|
+
result = validator.validate_token(token)
|
|
647
|
+
|
|
648
|
+
if result[:valid]
|
|
649
|
+
# ✅ Identificación del usuario
|
|
650
|
+
puts "User ID: #{result[:sub]}"
|
|
651
|
+
puts "Username: #{result['cognito:username'] || result[:username]}"
|
|
652
|
+
|
|
653
|
+
# ✅ Información de contacto
|
|
654
|
+
if result[:email]
|
|
655
|
+
puts "Email: #{result[:email]}"
|
|
656
|
+
puts "Email verificado: #{result[:email_verified]}"
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# ✅ Autorización basada en grupos
|
|
660
|
+
if result['cognito:groups']&.include?('admin')
|
|
661
|
+
puts "Usuario es administrador"
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
# ✅ Validación de tiempo
|
|
665
|
+
expires_at = Time.at(result[:exp])
|
|
666
|
+
puts "Token expira: #{expires_at}"
|
|
667
|
+
|
|
668
|
+
issued_at = Time.at(result[:iat])
|
|
669
|
+
puts "Token emitido: #{issued_at}"
|
|
670
|
+
|
|
671
|
+
# ✅ Atributos custom
|
|
672
|
+
if result['custom:tenant_id']
|
|
673
|
+
puts "Tenant ID: #{result['custom:tenant_id']}"
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# ✅ Scope OAuth2 (solo Access Tokens)
|
|
677
|
+
if result[:token_use] == 'access' && result[:scope]
|
|
678
|
+
scopes = result[:scope].split(' ')
|
|
679
|
+
puts "Permisos OAuth2: #{scopes}"
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Diferencias entre Access Token e ID Token
|
|
685
|
+
|
|
686
|
+
| Campo | Access Token | ID Token |
|
|
687
|
+
|-------|-------------|----------|
|
|
688
|
+
| **`token_use`** | `"access"` | `"id"` |
|
|
689
|
+
| **`scope`** | ✅ Incluido | ❌ No incluido |
|
|
690
|
+
| **`client_id`** | ✅ Incluido | ❌ No incluido |
|
|
691
|
+
| **`email`** | ❌ No incluido | ✅ Incluido |
|
|
692
|
+
| **`email_verified`** | ❌ No incluido | ✅ Incluido |
|
|
693
|
+
| **`phone_number`** | ❌ No incluido | ✅ Incluido |
|
|
694
|
+
| **Custom Attributes** | ❌ No incluido | ✅ Incluido |
|
|
695
|
+
| **Uso Principal** | Autorización en APIs | Información del usuario |
|
|
696
|
+
|
|
697
|
+
### Notas Importantes
|
|
698
|
+
|
|
699
|
+
- **Claims opcionales**: La disponibilidad de campos como `email`, `phone_number`, y `custom:*` depende de tu configuración de Cognito
|
|
700
|
+
- **Token Use**: Usa `result[:token_use]` para determinar el tipo de token y qué campos esperar
|
|
701
|
+
- **Timestamps**: Los campos `exp`, `iat`, y `auth_time` están en formato Unix timestamp (segundos desde 1970-01-01)
|
|
702
|
+
- **Custom Attributes**: Los atributos custom de Cognito tienen el prefijo `custom:` en sus nombres
|
|
703
|
+
- **Grupos Cognito**: Los grupos se almacenan en el array `cognito:groups` cuando están configurados
|
|
704
|
+
|
|
705
|
+
## 📦 Estructura de la Respuesta de Validación
|
|
706
|
+
|
|
707
|
+
### Respuesta Básica de Validación
|
|
708
|
+
|
|
709
|
+
```ruby
|
|
710
|
+
result = validator.validate_token(token)
|
|
711
|
+
|
|
712
|
+
# Estructura del resultado:
|
|
713
|
+
{
|
|
714
|
+
valid: true, # Boolean - Indica si el token es válido
|
|
715
|
+
sub: "user-id", # String - ID del usuario
|
|
716
|
+
username: "john.doe", # String - Nombre de usuario
|
|
717
|
+
token_use: "access", # String - Tipo de token
|
|
718
|
+
# ... más claims del token ...
|
|
719
|
+
error: nil # String - Mensaje de error (solo si valid: false)
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Respuesta con API Key
|
|
724
|
+
|
|
725
|
+
Cuando se valida con un API Key, el resultado incluye información adicional:
|
|
726
|
+
|
|
727
|
+
```ruby
|
|
728
|
+
result = validator.validate(token, api_key: api_key)
|
|
729
|
+
|
|
730
|
+
# Resultado completo:
|
|
731
|
+
{
|
|
732
|
+
valid: true,
|
|
733
|
+
sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
734
|
+
username: "john.doe",
|
|
735
|
+
token_use: "access",
|
|
736
|
+
# ... otros claims del token ...
|
|
737
|
+
|
|
738
|
+
# Información del API Key
|
|
739
|
+
api_key_data: {
|
|
740
|
+
name: "production-api-key",
|
|
741
|
+
permissions: ["auth:access", "users:read"],
|
|
742
|
+
app_id: "my-application",
|
|
743
|
+
scope: "client",
|
|
744
|
+
created_at: 1735603200000,
|
|
745
|
+
last_used: 1735689600000,
|
|
746
|
+
is_active: true,
|
|
747
|
+
metadata: {
|
|
748
|
+
created_for: "Integration Team",
|
|
749
|
+
description: "API Key for production integration",
|
|
750
|
+
environment: "production"
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
### Estructura de api_key_data
|
|
757
|
+
|
|
758
|
+
```ruby
|
|
759
|
+
{
|
|
760
|
+
# Nombre identificador del API Key
|
|
761
|
+
name: "production-api-key",
|
|
762
|
+
|
|
763
|
+
# Lista de permisos asignados
|
|
764
|
+
permissions: ["auth:access", "users:read"],
|
|
765
|
+
|
|
766
|
+
# ID de la aplicación asociada (opcional para scope 'system')
|
|
767
|
+
app_id: "my-application",
|
|
768
|
+
|
|
769
|
+
# Alcance del API Key
|
|
770
|
+
scope: "client", # Valores: 'app', 'system', 'client'
|
|
771
|
+
|
|
772
|
+
# Timestamp de creación (milisegundos)
|
|
773
|
+
created_at: 1735603200000,
|
|
774
|
+
|
|
775
|
+
# Timestamp del último uso (nil si nunca usado)
|
|
776
|
+
last_used: 1735689600000,
|
|
777
|
+
|
|
778
|
+
# Estado del API Key
|
|
779
|
+
is_active: true,
|
|
780
|
+
|
|
781
|
+
# Metadatos personalizados
|
|
782
|
+
metadata: {
|
|
783
|
+
created_for: "Integration Team",
|
|
784
|
+
environment: "production"
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
### Diferencias entre Scopes de API Keys
|
|
790
|
+
|
|
791
|
+
| Scope | Descripción | Restricciones | Uso Típico |
|
|
792
|
+
|-------|-------------|---------------|------------|
|
|
793
|
+
| **`system`** | Acceso transversal a todas las aplicaciones | Ninguna - acceso completo | Administración, integraciones de sistema |
|
|
794
|
+
| **`app`** | Acceso limitado a una aplicación específica | Solo puede acceder al `app_id` asociado | Integraciones de aplicaciones específicas |
|
|
795
|
+
| **`client`** | Acceso de cliente/frontend | Restricciones según permisos asignados | Aplicaciones frontend, móviles |
|
|
796
|
+
|
|
797
|
+
### Ejemplos de Uso con API Key
|
|
798
|
+
|
|
799
|
+
#### 1. Validación Básica con API Key
|
|
800
|
+
|
|
801
|
+
```ruby
|
|
802
|
+
result = validator.validate(token, api_key: api_key)
|
|
803
|
+
|
|
804
|
+
if result[:valid]
|
|
805
|
+
puts "✅ Token válido"
|
|
806
|
+
puts "Usuario: #{result[:username]}"
|
|
807
|
+
|
|
808
|
+
# Información del API Key
|
|
809
|
+
if result[:api_key_data]
|
|
810
|
+
key_data = result[:api_key_data]
|
|
811
|
+
puts "API Key: #{key_data[:name]}"
|
|
812
|
+
puts "Scope: #{key_data[:scope]}"
|
|
813
|
+
puts "Permisos: #{key_data[:permissions].join(', ')}"
|
|
814
|
+
puts "App ID: #{key_data[:app_id]}" if key_data[:app_id]
|
|
815
|
+
end
|
|
816
|
+
else
|
|
817
|
+
puts "❌ Token inválido: #{result[:error]}"
|
|
818
|
+
end
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
#### 2. Autorización basada en API Key Scope
|
|
822
|
+
|
|
823
|
+
```ruby
|
|
824
|
+
result = validator.validate(token, api_key: api_key)
|
|
825
|
+
|
|
826
|
+
if result[:valid] && result[:api_key_data]
|
|
827
|
+
key_data = result[:api_key_data]
|
|
828
|
+
|
|
829
|
+
# Verificar si tiene acceso system (transversal)
|
|
830
|
+
if key_data[:scope] == 'system'
|
|
831
|
+
puts "✅ Acceso system - puede acceder a todas las apps"
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
# Verificar si tiene acceso a app específica
|
|
835
|
+
if key_data[:scope] == 'app' && key_data[:app_id] == 'my-target-app'
|
|
836
|
+
puts "✅ Acceso autorizado a my-target-app"
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
# Verificar permisos específicos
|
|
840
|
+
if key_data[:permissions].include?('users:write')
|
|
841
|
+
puts "✅ Puede modificar usuarios"
|
|
842
|
+
end
|
|
843
|
+
end
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
#### 3. Usar Metadata del API Key
|
|
847
|
+
|
|
848
|
+
```ruby
|
|
849
|
+
result = validator.validate(token, api_key: api_key)
|
|
850
|
+
|
|
851
|
+
if result[:valid] && result[:api_key_data]
|
|
852
|
+
metadata = result[:api_key_data][:metadata]
|
|
853
|
+
|
|
854
|
+
# Acceder a información personalizada
|
|
855
|
+
puts "Creado para: #{metadata[:created_for]}"
|
|
856
|
+
puts "Descripción: #{metadata[:description]}"
|
|
857
|
+
puts "Ambiente: #{metadata[:environment]}"
|
|
858
|
+
|
|
859
|
+
# Control de acceso basado en ambiente
|
|
860
|
+
if metadata[:environment] == 'production'
|
|
861
|
+
Rails.logger.info "⚠️ API Key de producción - logging extra habilitado"
|
|
862
|
+
end
|
|
863
|
+
end
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
#### 4. Tracking de Último Uso
|
|
867
|
+
|
|
868
|
+
```ruby
|
|
869
|
+
result = validator.validate(token, api_key: api_key)
|
|
870
|
+
|
|
871
|
+
if result[:valid] && result[:api_key_data]
|
|
872
|
+
key_data = result[:api_key_data]
|
|
873
|
+
|
|
874
|
+
# Verificar última vez usado
|
|
875
|
+
if key_data[:last_used]
|
|
876
|
+
last_used_date = Time.at(key_data[:last_used] / 1000)
|
|
877
|
+
puts "Última vez usado: #{last_used_date}"
|
|
878
|
+
|
|
879
|
+
# Detectar API Keys inactivos (más de 30 días sin uso)
|
|
880
|
+
days_since_last_use = (Time.now - last_used_date) / (60 * 60 * 24)
|
|
881
|
+
if days_since_last_use > 30
|
|
882
|
+
puts "⚠️ API Key inactivo por más de 30 días"
|
|
883
|
+
end
|
|
884
|
+
else
|
|
885
|
+
puts "ℹ️ API Key nunca usado antes"
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# Antigüedad del API Key
|
|
889
|
+
created_date = Time.at(key_data[:created_at] / 1000)
|
|
890
|
+
puts "Creado el: #{created_date}"
|
|
891
|
+
end
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
### Respuesta con Datos Enriquecidos
|
|
895
|
+
|
|
896
|
+
Cuando usas `validate_enriched()`, obtienes campos adicionales:
|
|
897
|
+
|
|
898
|
+
```ruby
|
|
899
|
+
result = validator.validate_enriched(token, api_key)
|
|
900
|
+
|
|
901
|
+
# Resultado completo con datos enriquecidos:
|
|
902
|
+
{
|
|
903
|
+
valid: true,
|
|
904
|
+
sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
905
|
+
username: "john.doe",
|
|
906
|
+
email: "john.doe@example.com",
|
|
907
|
+
# ... otros campos del token ...
|
|
908
|
+
|
|
909
|
+
api_key_data: {
|
|
910
|
+
name: "production-api-key",
|
|
911
|
+
permissions: ["auth:access", "users:read"],
|
|
912
|
+
app_id: "my-application",
|
|
913
|
+
scope: "client",
|
|
914
|
+
created_at: 1735603200000,
|
|
915
|
+
last_used: 1735689600000,
|
|
916
|
+
is_active: true,
|
|
917
|
+
metadata: { environment: "production" }
|
|
918
|
+
},
|
|
919
|
+
|
|
920
|
+
user_permissions: {
|
|
921
|
+
"userId" => "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
922
|
+
"permissions" => {
|
|
923
|
+
"my-app" => {
|
|
924
|
+
"org-123" => {
|
|
925
|
+
"roles" => ["admin", "user"],
|
|
926
|
+
"effectivePermissions" => ["users:read", "users:write"],
|
|
927
|
+
"status" => "active"
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
|
|
933
|
+
user_organizations: [
|
|
934
|
+
{
|
|
935
|
+
"appId" => "my-app",
|
|
936
|
+
"organizationId" => "org-123",
|
|
937
|
+
"roles" => ["admin"],
|
|
938
|
+
"status" => "active",
|
|
939
|
+
"effectivePermissions" => ["users:read", "users:write"]
|
|
940
|
+
}
|
|
941
|
+
],
|
|
942
|
+
|
|
943
|
+
applications: [
|
|
944
|
+
{
|
|
945
|
+
"appId" => "my-app",
|
|
946
|
+
"name" => "My Application",
|
|
947
|
+
"isActive" => true,
|
|
948
|
+
"createdAt" => 1735603200000,
|
|
949
|
+
"updatedAt" => 1735689600000
|
|
950
|
+
}
|
|
951
|
+
]
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
### Manejo de Errores
|
|
956
|
+
|
|
957
|
+
Cuando la validación falla, obtienes un mensaje de error claro:
|
|
958
|
+
|
|
959
|
+
```ruby
|
|
960
|
+
result = validator.validate_token(token)
|
|
961
|
+
|
|
962
|
+
unless result[:valid]
|
|
963
|
+
puts "❌ Error: #{result[:error]}"
|
|
964
|
+
|
|
965
|
+
# Ejemplos de errores comunes:
|
|
966
|
+
# - "Token has expired"
|
|
967
|
+
# - "Invalid token signature"
|
|
968
|
+
# - "Invalid audience"
|
|
969
|
+
# - "Token has been revoked"
|
|
970
|
+
# - "API key is inactive"
|
|
971
|
+
# - "No access to application"
|
|
972
|
+
end
|
|
973
|
+
```
|
|
974
|
+
|
|
522
975
|
## Integración con Rails
|
|
523
976
|
|
|
524
977
|
### Ejemplo de Middleware
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
|
|
5
|
+
module JwtAuthCognito
|
|
6
|
+
# Rails Concern that adds granular permission enforcement to controllers.
|
|
7
|
+
#
|
|
8
|
+
# Usage in ApplicationController (or any controller):
|
|
9
|
+
#
|
|
10
|
+
# include JwtAuthCognito::AuthorizationConcern
|
|
11
|
+
#
|
|
12
|
+
# # Provide the validator instance (required):
|
|
13
|
+
# def jwt_validator
|
|
14
|
+
# @jwt_validator ||= JwtAuthCognito::JwtValidator.new
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # Provide the current user's Cognito sub (required):
|
|
18
|
+
# # Override jwt_user_id — typically populated by your auth before_action.
|
|
19
|
+
# def jwt_user_id
|
|
20
|
+
# @jwt_user_id ||= request.env['jwt.payload']&.dig('sub')
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# Then in any action or before_action:
|
|
24
|
+
#
|
|
25
|
+
# before_action -> { authorize_permission!('fleet:vehicles:read') }
|
|
26
|
+
#
|
|
27
|
+
# # OR require at least one of several permissions:
|
|
28
|
+
# before_action -> { authorize_any_permission!('fleet:read', 'fleet:vehicles:read') }
|
|
29
|
+
#
|
|
30
|
+
# appId and orgId are resolved from X-App-Id / X-Organization-Id headers,
|
|
31
|
+
# falling back to params[:appId] / params[:organizationId].
|
|
32
|
+
module AuthorizationConcern
|
|
33
|
+
extend ActiveSupport::Concern
|
|
34
|
+
|
|
35
|
+
included do
|
|
36
|
+
helper_method :current_user_permissions if respond_to?(:helper_method)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Raises ForbiddenError unless the current user has ALL of the given permissions.
|
|
40
|
+
def authorize_permission!(*permissions)
|
|
41
|
+
permissions.flatten.each do |permission|
|
|
42
|
+
next if current_user_has_permission?(permission)
|
|
43
|
+
|
|
44
|
+
log_permission_denied(permissions)
|
|
45
|
+
raise JwtAuthCognito::ForbiddenError, 'Access denied'
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Raises ForbiddenError unless the current user has AT LEAST ONE of the given permissions.
|
|
50
|
+
def authorize_any_permission!(*permissions)
|
|
51
|
+
return if permissions.flatten.any? { |p| current_user_has_permission?(p) }
|
|
52
|
+
|
|
53
|
+
log_permission_denied(permissions)
|
|
54
|
+
raise JwtAuthCognito::ForbiddenError, 'Access denied'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns the full list of effective permissions for the current user/app/org context.
|
|
58
|
+
def current_user_permissions
|
|
59
|
+
@current_user_permissions ||= fetch_current_user_permissions
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def fetch_current_user_permissions
|
|
65
|
+
uid = jwt_user_id
|
|
66
|
+
aid = jwt_app_id
|
|
67
|
+
oid = jwt_org_id
|
|
68
|
+
return [] unless uid && aid && oid
|
|
69
|
+
|
|
70
|
+
jwt_validator.resolve_effective_permissions_for(uid, aid, oid) || []
|
|
71
|
+
rescue StandardError
|
|
72
|
+
[]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def current_user_has_permission?(permission)
|
|
76
|
+
PermissionChecker.permission_in_list?(permission, current_user_permissions)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Override in your ApplicationController to provide the Cognito sub.
|
|
80
|
+
# Typically populated by a JWT authentication before_action.
|
|
81
|
+
def jwt_user_id
|
|
82
|
+
@jwt_user_id
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Resolves appId from X-App-Id header → params[:appId].
|
|
86
|
+
def jwt_app_id
|
|
87
|
+
@jwt_app_id ||=
|
|
88
|
+
request.headers['X-App-Id'] ||
|
|
89
|
+
request.headers['x-app-id'] ||
|
|
90
|
+
params[:appId]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Resolves orgId from X-Organization-Id header → params[:organizationId].
|
|
94
|
+
def jwt_org_id
|
|
95
|
+
@jwt_org_id ||=
|
|
96
|
+
request.headers['X-Organization-Id'] ||
|
|
97
|
+
request.headers['x-organization-id'] ||
|
|
98
|
+
params[:organizationId]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Override in your ApplicationController to provide the JwtValidator instance.
|
|
102
|
+
def jwt_validator
|
|
103
|
+
raise NotImplementedError,
|
|
104
|
+
'You must implement `jwt_validator` in your controller. ' \
|
|
105
|
+
'Example: def jwt_validator; @jwt_validator ||= JwtAuthCognito::JwtValidator.new; end'
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def log_permission_denied(permissions)
|
|
109
|
+
return unless defined?(Rails)
|
|
110
|
+
|
|
111
|
+
Rails.logger.warn(
|
|
112
|
+
"[PERMISSION_DENIED] user_id=#{jwt_user_id} " \
|
|
113
|
+
"permissions=#{Array(permissions).flatten.join(',')} " \
|
|
114
|
+
"app_id=#{jwt_app_id} org_id=#{jwt_org_id}"
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -23,6 +23,13 @@ module JwtAuthCognito
|
|
|
23
23
|
raise ValidationError, 'Token missing key ID (kid)' unless kid
|
|
24
24
|
|
|
25
25
|
public_key = get_public_key(kid)
|
|
26
|
+
# First decode to check token type before audience validation
|
|
27
|
+
payload_preview = JWT.decode(token, nil, false).first
|
|
28
|
+
|
|
29
|
+
# Only verify audience for ID tokens, not access tokens
|
|
30
|
+
# Access tokens from Cognito might not have 'aud' claim
|
|
31
|
+
should_verify_aud = @config.cognito_client_id && payload_preview['token_use'] == 'id'
|
|
32
|
+
|
|
26
33
|
decoded_token = JWT.decode(
|
|
27
34
|
token,
|
|
28
35
|
public_key,
|
|
@@ -31,8 +38,8 @@ module JwtAuthCognito
|
|
|
31
38
|
algorithm: 'RS256',
|
|
32
39
|
iss: @config.cognito_issuer,
|
|
33
40
|
verify_iss: true,
|
|
34
|
-
aud: @config.cognito_client_id,
|
|
35
|
-
verify_aud:
|
|
41
|
+
aud: should_verify_aud ? @config.cognito_client_id : nil,
|
|
42
|
+
verify_aud: should_verify_aud
|
|
36
43
|
}
|
|
37
44
|
)
|
|
38
45
|
|
|
@@ -224,6 +224,52 @@ module JwtAuthCognito
|
|
|
224
224
|
@config.has_client_secret?
|
|
225
225
|
end
|
|
226
226
|
|
|
227
|
+
# ========== PERMISSION CHECKING ==========
|
|
228
|
+
|
|
229
|
+
# Returns true if the user has the given permission in the specified app/org context.
|
|
230
|
+
def has_permission?(user_id, app_id, org_id, permission)
|
|
231
|
+
permissions = resolve_effective_permissions_for(user_id, app_id, org_id)
|
|
232
|
+
return false unless permissions
|
|
233
|
+
|
|
234
|
+
PermissionChecker.permission_in_list?(permission, permissions)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Bulk permission check. Returns a hash with :allowed, :denied, :has_all, :has_any.
|
|
238
|
+
def check_permissions(user_id, app_id, org_id, permissions_to_check)
|
|
239
|
+
effective = resolve_effective_permissions_for(user_id, app_id, org_id) || []
|
|
240
|
+
|
|
241
|
+
allowed = []
|
|
242
|
+
denied = []
|
|
243
|
+
|
|
244
|
+
Array(permissions_to_check).each do |perm|
|
|
245
|
+
if PermissionChecker.permission_in_list?(perm, effective)
|
|
246
|
+
allowed << perm
|
|
247
|
+
else
|
|
248
|
+
denied << perm
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
{ allowed: allowed, denied: denied, has_all: denied.empty?, has_any: allowed.any? }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Validates a Bearer token and checks a single permission in one call.
|
|
256
|
+
def has_permission_from_token?(token, app_id, org_id, permission)
|
|
257
|
+
result = validate_token(token)
|
|
258
|
+
return false unless result[:valid]
|
|
259
|
+
|
|
260
|
+
user_id = result[:payload]&.dig('sub')
|
|
261
|
+
return false unless user_id
|
|
262
|
+
|
|
263
|
+
has_permission?(user_id, app_id, org_id, permission)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Exposes effective permissions via the user data service.
|
|
267
|
+
def resolve_effective_permissions_for(user_id, app_id, org_id)
|
|
268
|
+
return nil unless @user_data_service
|
|
269
|
+
|
|
270
|
+
@user_data_service.resolve_effective_permissions(user_id, app_id, org_id)
|
|
271
|
+
end
|
|
272
|
+
|
|
227
273
|
def is_token_expired?(token)
|
|
228
274
|
payload = decode_token(token)
|
|
229
275
|
return true if payload.is_a?(Hash) && payload[:error]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JwtAuthCognito
|
|
4
|
+
module PermissionChecker
|
|
5
|
+
# Checks whether a permission string is satisfied by any entry in permission_list.
|
|
6
|
+
# Supports wildcard patterns:
|
|
7
|
+
# * — global wildcard (matches everything)
|
|
8
|
+
# module:* — prefix wildcard (matches module:action AND module:sub:action)
|
|
9
|
+
# module:submodule:* — narrow prefix wildcard
|
|
10
|
+
# *.action — suffix wildcard (matches last segment across any depth)
|
|
11
|
+
def self.permission_in_list?(permission, permission_list)
|
|
12
|
+
return false if permission_list.nil? || permission_list.empty?
|
|
13
|
+
return true if permission_list.include?(permission)
|
|
14
|
+
return true if permission_list.include?('*')
|
|
15
|
+
|
|
16
|
+
permission_list.each do |p|
|
|
17
|
+
if p.end_with?(':*')
|
|
18
|
+
prefix = p[0..-3]
|
|
19
|
+
return true if permission.start_with?("#{prefix}:")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if p.end_with?('.*')
|
|
23
|
+
prefix = p[0..-3]
|
|
24
|
+
return true if permission.start_with?("#{prefix}.")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
next unless p.start_with?('*.')
|
|
28
|
+
|
|
29
|
+
action = p[2..]
|
|
30
|
+
return true if permission.end_with?(":#{action}") || permission.end_with?(".#{action}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -192,6 +192,49 @@ module JwtAuthCognito
|
|
|
192
192
|
end
|
|
193
193
|
end
|
|
194
194
|
|
|
195
|
+
# Resolves effective permissions for a user in a specific app/org context.
|
|
196
|
+
# Fallback chain:
|
|
197
|
+
# 1. permissions:cache:{userId}:{appId}:{orgId} (handles both plain [] and { permissions: [] })
|
|
198
|
+
# 2. user:permissions:{userId} → permissions[appId][orgId].effectivePermissions
|
|
199
|
+
# 3. Compute from roles using app:roles:{appId}:{orgId}
|
|
200
|
+
# Returns nil if the user has no active membership in that org.
|
|
201
|
+
def resolve_effective_permissions(user_id, app_id, organization_id)
|
|
202
|
+
# Step 1: permissions cache
|
|
203
|
+
begin
|
|
204
|
+
raw = @redis_service.get("permissions:cache:#{user_id}:#{app_id}:#{organization_id}")
|
|
205
|
+
if raw
|
|
206
|
+
parsed = JSON.parse(raw)
|
|
207
|
+
return parsed if parsed.is_a?(Array)
|
|
208
|
+
return parsed['permissions'] if parsed.is_a?(Hash) && parsed['permissions'].is_a?(Array)
|
|
209
|
+
end
|
|
210
|
+
rescue StandardError => e
|
|
211
|
+
puts "Error reading permissions cache for #{user_id}: #{e.message}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Steps 2 & 3: user:permissions
|
|
215
|
+
begin
|
|
216
|
+
raw = @redis_service.get("user:permissions:#{user_id}")
|
|
217
|
+
return nil unless raw
|
|
218
|
+
|
|
219
|
+
data = JSON.parse(raw)
|
|
220
|
+
org_data = data.dig('permissions', app_id, organization_id)
|
|
221
|
+
return nil unless org_data
|
|
222
|
+
return nil unless org_data['status'] == 'active'
|
|
223
|
+
|
|
224
|
+
effective = org_data['effectivePermissions']
|
|
225
|
+
return effective if effective.is_a?(Array)
|
|
226
|
+
|
|
227
|
+
# Step 3: compute from role definitions
|
|
228
|
+
roles = org_data['roles']
|
|
229
|
+
return [] unless roles.is_a?(Array) && !roles.empty?
|
|
230
|
+
|
|
231
|
+
compute_permissions_from_roles(app_id, organization_id, roles)
|
|
232
|
+
rescue StandardError => e
|
|
233
|
+
puts "Error resolving effective permissions for #{user_id}: #{e.message}"
|
|
234
|
+
nil
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
195
238
|
def get_effective_permissions(user_id, app_id, organization_id)
|
|
196
239
|
return nil unless @config[:include_effective_permissions]
|
|
197
240
|
|
|
@@ -328,5 +371,24 @@ module JwtAuthCognito
|
|
|
328
371
|
@cache[key] = value
|
|
329
372
|
@cache_timestamps[key] = Time.now.to_i
|
|
330
373
|
end
|
|
374
|
+
|
|
375
|
+
def compute_permissions_from_roles(app_id, organization_id, role_names)
|
|
376
|
+
roles_data = get_app_roles(app_id, organization_id)
|
|
377
|
+
return [] unless roles_data.is_a?(Hash)
|
|
378
|
+
|
|
379
|
+
permissions = []
|
|
380
|
+
role_names.each do |role_name|
|
|
381
|
+
role = roles_data[role_name]
|
|
382
|
+
next unless role.is_a?(Hash)
|
|
383
|
+
|
|
384
|
+
role_permissions = role['permissions']
|
|
385
|
+
permissions.concat(role_permissions) if role_permissions.is_a?(Array)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
permissions.uniq
|
|
389
|
+
rescue StandardError => e
|
|
390
|
+
puts "Error computing permissions from roles: #{e.message}"
|
|
391
|
+
[]
|
|
392
|
+
end
|
|
331
393
|
end
|
|
332
394
|
end
|
data/lib/jwt_auth_cognito.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative 'jwt_auth_cognito/token_blacklist_service'
|
|
|
9
9
|
require_relative 'jwt_auth_cognito/api_key_validator'
|
|
10
10
|
require_relative 'jwt_auth_cognito/user_data_service'
|
|
11
11
|
require_relative 'jwt_auth_cognito/error_utils'
|
|
12
|
+
require_relative 'jwt_auth_cognito/permission_checker'
|
|
12
13
|
require_relative 'jwt_auth_cognito/jwt_validator'
|
|
13
14
|
|
|
14
15
|
module JwtAuthCognito
|
|
@@ -16,6 +17,7 @@ module JwtAuthCognito
|
|
|
16
17
|
class ValidationError < Error; end
|
|
17
18
|
class BlacklistError < Error; end
|
|
18
19
|
class ConfigurationError < Error; end
|
|
20
|
+
class ForbiddenError < Error; end
|
|
19
21
|
|
|
20
22
|
# Specific JWT error types (matching Node.js implementation)
|
|
21
23
|
class TokenExpiredError < ValidationError; end
|
|
@@ -71,3 +73,6 @@ end
|
|
|
71
73
|
|
|
72
74
|
# Cargar tareas Rake si estamos en Rails
|
|
73
75
|
require_relative 'jwt_auth_cognito/railtie' if defined?(Rails::Railtie)
|
|
76
|
+
|
|
77
|
+
# AuthorizationConcern requires ActiveSupport (available in Rails apps)
|
|
78
|
+
require_relative 'jwt_auth_cognito/authorization_concern' if defined?(ActiveSupport::Concern)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jwt_auth_cognito
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.0.pre.beta.
|
|
4
|
+
version: 1.0.0.pre.beta.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- The Optimal
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-06-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: aws-sdk-ssm
|
|
@@ -189,10 +189,12 @@ files:
|
|
|
189
189
|
- lib/generators/jwt_auth_cognito/templates/jwt_auth_cognito.rb.erb
|
|
190
190
|
- lib/jwt_auth_cognito.rb
|
|
191
191
|
- lib/jwt_auth_cognito/api_key_validator.rb
|
|
192
|
+
- lib/jwt_auth_cognito/authorization_concern.rb
|
|
192
193
|
- lib/jwt_auth_cognito/configuration.rb
|
|
193
194
|
- lib/jwt_auth_cognito/error_utils.rb
|
|
194
195
|
- lib/jwt_auth_cognito/jwks_service.rb
|
|
195
196
|
- lib/jwt_auth_cognito/jwt_validator.rb
|
|
197
|
+
- lib/jwt_auth_cognito/permission_checker.rb
|
|
196
198
|
- lib/jwt_auth_cognito/railtie.rb
|
|
197
199
|
- lib/jwt_auth_cognito/redis_service.rb
|
|
198
200
|
- lib/jwt_auth_cognito/ssm_service.rb
|