jwt_auth_cognito 1.0.0.pre.beta.12 → 1.0.0.pre.beta.14

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b17cfc63543d51251188fa8645cf7f9235d741851fc33087bf781cb26ba21c07
4
- data.tar.gz: c51dd33e95f2346794ad4d50578982e09ed0395b22e192d8a69e4a196ef8c687
3
+ metadata.gz: 77eed5e47f35c5f0a7e693bab6c09e70cf2ea950670273d203ff3611d94b8a70
4
+ data.tar.gz: 21a499f3e1ad917bf9587cd8695e2dc44cceb7fb77f3ada93185ae2ed855c0d7
5
5
  SHA512:
6
- metadata.gz: eb6034d5dcb76d9523e4dcf68e488899b2ee50eba313aba072b069a4a552d8cc331175d72dca2577b7f85b9e3b63720dbafe98a21d7cf10107da457cab281436
7
- data.tar.gz: 7e5e07f954ca37e0ece2cfed27763426903623aafb3a2f37dd9fecf9c54a876ace20ec7e9e0fea7e8ea8117bc34af28ecd730af22c387a196b43b704a4a40925
6
+ metadata.gz: 2fb3defb3e11e9cc318ac1d387cf58c99fa6b1b3152bc34841db21645fb6a6fa67ab99af956fb4fd539aeaa44132df9994bc55256f9bb4412e30ebd112801c6a
7
+ data.tar.gz: cba58be12f1fd5dfe1d199a83e27e60fa851bedfd1b39a2d7b8a0f6c2669c99c9f4459fbe6390977aef177c1c797abecd83ffb447e966615d6556ac8192acf3c
data/CHANGELOG.md CHANGED
@@ -7,6 +7,49 @@ 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.14] - 2026-06-18
11
+
12
+ ### Changed
13
+
14
+ - **`create_cognito_validator` enables identity enrichment by default.** The
15
+ convenience constructor always receives the region, so
16
+ `validate(token, enrich_user_data: true)` now returns `payload['email']`
17
+ without extra setup. Pass `enable_user_identity_enrichment: false` to opt out.
18
+ The `configure` + `JwtValidator.new` path stays opt-in (gated on
19
+ `config.enable_user_identity_enrichment`), since it may run without Cognito
20
+ network access. Mirrors the npm package's `createCognitoValidatorAsync` default.
21
+
22
+ ### Fixed
23
+
24
+ - **`RedisService#set` hardened against `SETEX key 0`**: `0` is truthy in Ruby, so
25
+ `set(key, value, 0)` would have issued `SETEX key 0` (rejected by Redis with
26
+ "ERR invalid expire time"). `set` now attaches an expiry only for a positive TTL;
27
+ a nil/non-positive ttl persists the key with a plain `SET`. Current callers were
28
+ unaffected (`update_last_used` passes no TTL); this prevents the footgun and
29
+ matches the npm package's `RedisService.set` behavior.
30
+
31
+ ## [1.0.0-beta.13] - 2026-06-17
32
+
33
+ ### Added
34
+
35
+ - **Cognito identity enrichment** (populate `payload['email']` on ACCESS tokens):
36
+ - Cognito **access tokens do not carry `email`** (only ID tokens do). New opt-in
37
+ enrichment fetches the user's standard identity attributes via Cognito `GetUser`
38
+ — authorized by the access token's own `aws.cognito.signin.user.admin` scope, so
39
+ **no AWS IAM credentials are required** — and merges them into the decoded payload.
40
+ - Enable with `config.enable_user_identity_enrichment = true` (env
41
+ `ENABLE_USER_IDENTITY_ENRICHMENT=true`), or pass
42
+ `enable_user_identity_enrichment: true` to `create_cognito_validator`.
43
+ - Runs only for `token_use == 'access'` tokens that don't already have `email`,
44
+ and only when `enrich_user_data` is requested (default in `validate`).
45
+ - Results are cached per `sub` (default 300s, `IDENTITY_CACHE_TIMEOUT`). Any failure
46
+ is non-fatal: the token still validates, just without identity enrichment.
47
+ - Default merged attributes: `email`, `email_verified`, `name`, `given_name`,
48
+ `family_name`, `phone_number`, `phone_number_verified`, `preferred_username`
49
+ (`*_verified` normalized to boolean). `custom:*` attributes are NOT merged.
50
+ - New `JwtAuthCognito::CognitoIdentityService` class.
51
+ - New dependency: `aws-sdk-cognitoidentityprovider`.
52
+
10
53
  ## [1.0.0-beta.11] - 2025-01-23
11
54
 
12
55
  ### Improved
data/CLAUDE.md CHANGED
@@ -69,6 +69,7 @@ rake jwt_auth_cognito:test_cognito # Test Cognito connection
69
69
  - **RedisService**: Low-level Redis operations with comprehensive TLS support and retry logic
70
70
  - **TokenBlacklistService**: High-level token revocation and blacklist management
71
71
  - **UserDataService**: User data retrieval from Redis with caching and auth-service compatibility
72
+ - **CognitoIdentityService**: Fetches identity attributes (email, name, ...) via Cognito `GetUser` to enrich ACCESS tokens, which don't carry `email`. `GetUser` is authorized by the access token's own `aws.cognito.signin.user.admin` scope — **no AWS IAM credentials required**. Opt-in via `enable_user_identity_enrichment`; merges into `payload` in `validate` only for `token_use == 'access'` tokens missing `email` when `enrich_user_data` is on; per-`sub` cache (default 300s); failures are non-fatal; `custom:*` excluded; `*_verified` normalized to boolean. Dependency: `aws-sdk-cognitoidentityprovider`.
72
73
  - **ApiKeyValidator**: API key validation with system and app-level access control
73
74
  - **ErrorUtils**: Centralized error handling and categorization system
74
75
  - **SSMService**: AWS Parameter Store integration for secure certificate management (auth-service compatible)
data/README.md CHANGED
@@ -102,6 +102,8 @@ AWS_SSM_ENDPOINT=https://ssm.us-east-1.amazonaws.com # Opcional, para VPC endpo
102
102
  # Habilitar funcionalidades específicas
103
103
  ENABLE_API_KEY_VALIDATION=true # Validación de API keys
104
104
  ENABLE_USER_DATA_RETRIEVAL=true # Enriquecimiento de datos de usuario
105
+ ENABLE_USER_IDENTITY_ENRICHMENT=true # Enriquecimiento de identidad (email en access tokens)
106
+ IDENTITY_CACHE_TIMEOUT=300 # TTL de caché de identidad por sub (segundos)
105
107
  ```
106
108
 
107
109
  ### Opciones de Configuración Boolean
@@ -110,9 +112,34 @@ La gema soporta las siguientes opciones boolean para habilitar funcionalidades e
110
112
 
111
113
  - **`enable_api_key_validation`** - Habilita la validación de API keys para control de acceso a nivel de sistema y aplicación (default: false)
112
114
  - **`enable_user_data_retrieval`** - Habilita el enriquecimiento de datos de usuario con permisos, organizaciones y aplicaciones (default: false)
115
+ - **`enable_user_identity_enrichment`** - Habilita el enriquecimiento de identidad: pobla `payload['email']` (y otros atributos estándar) en **access tokens** vía Cognito `GetUser` (default: false)
113
116
 
114
117
  Estas opciones permiten control granular sobre qué características están activas, optimizando el rendimiento habilitando solo la funcionalidad necesaria.
115
118
 
119
+ ### Enriquecimiento de Identidad — `email` en Access Tokens
120
+
121
+ Los **access tokens de Cognito NO incluyen `email`** (solo los ID tokens lo traen). Si tu backend valida el access token y necesita el email (p. ej. para trazabilidad/logging), habilita `enable_user_identity_enrichment`. La gema obtiene los atributos del usuario con `GetUser` —autorizado por el propio scope `aws.cognito.signin.user.admin` del access token, **sin credenciales AWS IAM**— y los fusiona en el `payload`.
122
+
123
+ > **Con `create_cognito_validator(...)` está activado por defecto** (ese constructor siempre recibe la región): `validate(token, enrich_user_data: true)` ya trae `payload['email']`. Para desactivarlo pasa `enable_user_identity_enrichment: false`. El path `configure` + `JwtValidator.new` de abajo sí requiere el flag explícito (puede correr sin acceso de red a Cognito).
124
+
125
+ ```ruby
126
+ JwtAuthCognito.configure do |config|
127
+ config.cognito_region = 'us-east-1'
128
+ config.enable_user_identity_enrichment = true
129
+ config.identity_cache_timeout = 300 # TTL de caché por sub (segundos)
130
+ end
131
+
132
+ result = validator.validate(access_token, api_key: api_key, enrich_user_data: true)
133
+ result[:payload]['email'] # ✅ ahora disponible
134
+ ```
135
+
136
+ **Comportamiento:**
137
+ - Solo actúa sobre tokens `token_use == 'access'` que aún no traen `email`. Los ID tokens se omiten (ya lo traen).
138
+ - Solo se ejecuta cuando `enrich_user_data` está activo (default en `validate`).
139
+ - Resultados cacheados por `sub` (default 300s) para no llamar a Cognito en cada request.
140
+ - Tolerante a fallos: si `GetUser` falla, el token sigue siendo válido (solo no se enriquece).
141
+ - Los `*_verified` se normalizan a boolean. Los atributos `custom:*` **no** se fusionan.
142
+
116
143
  ## Configuración AWS para Development
117
144
 
118
145
  ### Desarrollo Local
@@ -42,6 +42,7 @@ Gem::Specification.new do |spec|
42
42
  spec.extra_rdoc_files = ['README.md', 'CHANGELOG.md', 'LICENSE.txt']
43
43
 
44
44
  # Dependencies - compatible with llegando-neo (Ruby 2.7.5, Rails 5.2.6)
45
+ spec.add_dependency 'aws-sdk-cognitoidentityprovider', '~> 1.0' # For GetUser identity enrichment
45
46
  spec.add_dependency 'aws-sdk-ssm', '~> 1.0' # For AWS Parameter Store support
46
47
  spec.add_dependency 'json', '~> 2.0'
47
48
  spec.add_dependency 'jwt', '~> 2.0'
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-cognitoidentityprovider'
4
+
5
+ module JwtAuthCognito
6
+ # Fetches a user's Cognito identity attributes (email, name, ...) using the
7
+ # caller's OWN access token. Cognito access tokens do not carry `email`; only
8
+ # ID tokens do. The access token scope `aws.cognito.signin.user.admin`
9
+ # authorizes the GetUser operation, so NO AWS IAM credentials are required —
10
+ # the token itself is the authorization.
11
+ #
12
+ # Results are cached per-user (sub) so we don't hit Cognito on every single
13
+ # validation. Any failure returns nil (never raises) so token validation is
14
+ # never blocked by an identity-enrichment problem.
15
+ class CognitoIdentityService
16
+ # Curated STANDARD identity attributes merged into the decoded token.
17
+ # Intentionally excludes custom:* attributes to avoid leaking arbitrary data.
18
+ DEFAULT_IDENTITY_ATTRIBUTES = %w[
19
+ email email_verified name given_name family_name
20
+ phone_number phone_number_verified preferred_username
21
+ ].freeze
22
+
23
+ def initialize(config = JwtAuthCognito.configuration)
24
+ @region = config.cognito_region
25
+ @cache_ttl = config.identity_cache_timeout || 300
26
+ configured = config.identity_attributes
27
+ @attributes = configured && !configured.empty? ? configured : DEFAULT_IDENTITY_ATTRIBUTES
28
+ @cache = {}
29
+ @mutex = Mutex.new
30
+ end
31
+
32
+ # Returns a hash of identity attribute name => value for the user owning the
33
+ # access token, or nil on any failure (never raises).
34
+ def get_identity_attributes(access_token, sub = nil)
35
+ cache_key = sub || "token:#{access_token[-24..] || access_token}"
36
+
37
+ cached = read_cache(cache_key)
38
+ return cached if cached
39
+
40
+ response = client.get_user(access_token: access_token)
41
+
42
+ all = {}
43
+ response.user_attributes.each { |attr| all[attr.name] = attr.value }
44
+
45
+ filtered = {}
46
+ @attributes.each { |name| filtered[name] = all[name] if all.key?(name) }
47
+
48
+ write_cache(cache_key, filtered)
49
+ filtered
50
+ rescue StandardError => e
51
+ ErrorUtils.log_error(e, 'CognitoIdentityService.get_identity_attributes failed')
52
+ nil
53
+ end
54
+
55
+ def clear_cache
56
+ @mutex.synchronize { @cache = {} }
57
+ end
58
+
59
+ private
60
+
61
+ def client
62
+ @client ||= Aws::CognitoIdentityProvider::Client.new(region: @region)
63
+ end
64
+
65
+ def read_cache(key)
66
+ @mutex.synchronize do
67
+ entry = @cache[key]
68
+ next nil unless entry
69
+ next nil if entry[:expires_at] < Time.now.to_i
70
+
71
+ entry[:value]
72
+ end
73
+ end
74
+
75
+ def write_cache(key, value)
76
+ @mutex.synchronize do
77
+ @cache[key] = { value: value, expires_at: Time.now.to_i + @cache_ttl }
78
+ end
79
+ end
80
+ end
81
+ end
@@ -7,7 +7,8 @@ module JwtAuthCognito
7
7
  :redis_ssl, :redis_timeout, :redis_connect_timeout, :redis_read_timeout,
8
8
  :redis_ca_cert_path, :redis_ca_cert_name, :redis_verify_mode,
9
9
  :jwks_cache_ttl, :validation_mode, :environment,
10
- :enable_api_key_validation, :enable_user_data_retrieval
10
+ :enable_api_key_validation, :enable_user_data_retrieval,
11
+ :enable_user_identity_enrichment, :identity_cache_timeout, :identity_attributes
11
12
 
12
13
  def initialize
13
14
  @cognito_region = ENV['COGNITO_REGION'] || ENV['AWS_REGION'] || 'us-east-1'
@@ -35,6 +36,12 @@ module JwtAuthCognito
35
36
  @validation_mode = production? ? :secure : :basic
36
37
  @enable_api_key_validation = ENV['ENABLE_API_KEY_VALIDATION'] == 'true'
37
38
  @enable_user_data_retrieval = ENV['ENABLE_USER_DATA_RETRIEVAL'] == 'true'
39
+
40
+ # Cognito identity enrichment: populate payload['email'] (and other identity
41
+ # attributes) for ACCESS tokens via GetUser, since access tokens don't carry them.
42
+ @enable_user_identity_enrichment = ENV['ENABLE_USER_IDENTITY_ENRICHMENT'] == 'true'
43
+ @identity_cache_timeout = (ENV['IDENTITY_CACHE_TIMEOUT'] || 300).to_i
44
+ @identity_attributes = nil
38
45
  end
39
46
 
40
47
  def production?
@@ -10,6 +10,7 @@ module JwtAuthCognito
10
10
  @blacklist_service = TokenBlacklistService.new(config)
11
11
  @api_key_validator = config.enable_api_key_validation ? ApiKeyValidator.new(config) : nil
12
12
  @user_data_service = config.enable_user_data_retrieval ? UserDataService.new(nil, config.user_data_config) : nil
13
+ @cognito_identity_service = config.enable_user_identity_enrichment ? CognitoIdentityService.new(config) : nil
13
14
  @initialized = false
14
15
  end
15
16
 
@@ -94,6 +95,18 @@ module JwtAuthCognito
94
95
  end
95
96
  end
96
97
 
98
+ # Step 6: Enrich token claims with Cognito identity attributes (email, name, ...)
99
+ # Cognito ACCESS tokens do NOT carry email (only ID tokens do). Fetch the user's
100
+ # attributes via GetUser (authorized by the access token's own scope) and merge
101
+ # them into the payload so consumers get payload['email'] transparently.
102
+ if enrich_user_data && @config.enable_user_identity_enrichment && @cognito_identity_service
103
+ payload = enriched_result[:payload]
104
+ if payload && payload['token_use'] == 'access' && payload['email'].nil?
105
+ attrs = @cognito_identity_service.get_identity_attributes(token, payload['sub'])
106
+ apply_identity_attributes(payload, attrs) if attrs
107
+ end
108
+ end
109
+
97
110
  enriched_result
98
111
  end
99
112
 
@@ -445,5 +458,15 @@ module JwtAuthCognito
445
458
  { valid: true } # Continue with basic validation if user data service fails
446
459
  end
447
460
  end
461
+
462
+ # Merge fetched Cognito identity attributes into the decoded payload without
463
+ # overriding existing claims. `*_verified` attributes are normalized to booleans.
464
+ def apply_identity_attributes(payload, attrs)
465
+ attrs.each do |name, value|
466
+ next unless payload[name].nil?
467
+
468
+ payload[name] = name.end_with?('_verified') ? value == 'true' : value
469
+ end
470
+ end
448
471
  end
449
472
  end
@@ -105,7 +105,10 @@ module JwtAuthCognito
105
105
 
106
106
  def set(key, value, ttl = nil)
107
107
  connect_redis
108
- if ttl
108
+ # Only attach an expiry for a positive TTL. A nil or non-positive ttl (note:
109
+ # 0 is truthy in Ruby) means a persistent key — `SETEX key 0` is rejected by
110
+ # Redis with "ERR invalid expire time", so fall back to a plain SET.
111
+ if ttl&.positive?
109
112
  @redis.setex(key, ttl, value)
110
113
  else
111
114
  @redis.set(key, value)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JwtAuthCognito
4
- VERSION = '1.0.0-beta.12'
4
+ VERSION = '1.0.0-beta.14'
5
5
  end
@@ -8,6 +8,7 @@ require_relative 'jwt_auth_cognito/redis_service'
8
8
  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
+ require_relative 'jwt_auth_cognito/cognito_identity_service'
11
12
  require_relative 'jwt_auth_cognito/error_utils'
12
13
  require_relative 'jwt_auth_cognito/permission_checker'
13
14
  require_relative 'jwt_auth_cognito/jwt_validator'
@@ -41,8 +42,13 @@ module JwtAuthCognito
41
42
  end
42
43
 
43
44
  # Convenience factory method to create a Cognito validator
45
+ # Convenience constructor. Identity enrichment (email on access tokens) is ON
46
+ # by default here because the region is always provided; pass
47
+ # enable_user_identity_enrichment: false to opt out. (The configure +
48
+ # JwtValidator.new path keeps it opt-in, gated on config.enable_user_identity_enrichment.)
44
49
  def self.create_cognito_validator(region:, user_pool_id:, client_id: nil, client_secret: nil, redis_config: {},
45
- enable_api_key_validation: false, enable_user_data_retrieval: false)
50
+ enable_api_key_validation: false, enable_user_data_retrieval: false,
51
+ enable_user_identity_enrichment: true)
46
52
  old_config = configuration.dup
47
53
 
48
54
  configure do |config|
@@ -52,6 +58,7 @@ module JwtAuthCognito
52
58
  config.cognito_client_secret = client_secret if client_secret
53
59
  config.enable_api_key_validation = enable_api_key_validation
54
60
  config.enable_user_data_retrieval = enable_user_data_retrieval
61
+ config.enable_user_identity_enrichment = enable_user_identity_enrichment
55
62
 
56
63
  # Apply Redis configuration
57
64
  config.redis_host = redis_config[:host] if redis_config[:host]
metadata CHANGED
@@ -1,15 +1,29 @@
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.12
4
+ version: 1.0.0.pre.beta.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Optimal
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-08 00:00:00.000000000 Z
11
+ date: 2026-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-cognitoidentityprovider
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: aws-sdk-ssm
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -190,6 +204,7 @@ files:
190
204
  - lib/jwt_auth_cognito.rb
191
205
  - lib/jwt_auth_cognito/api_key_validator.rb
192
206
  - lib/jwt_auth_cognito/authorization_concern.rb
207
+ - lib/jwt_auth_cognito/cognito_identity_service.rb
193
208
  - lib/jwt_auth_cognito/configuration.rb
194
209
  - lib/jwt_auth_cognito/error_utils.rb
195
210
  - lib/jwt_auth_cognito/jwks_service.rb