jwt_auth_cognito 1.0.0.pre.beta.11 → 1.0.0.pre.beta.13

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: e8069ccdfaed845402cd9053a2db66b2bc74359649de17ca2ce7ad010a939722
4
- data.tar.gz: 3ba1441f8016d8e2d5b1bca7913fd7700f0ae51c8470ab1e6f06a2e1a9640697
3
+ metadata.gz: 21369d98a11f9f2dc8a1539f4f4db6071822ae94901f15e0c4eb3496d4d74585
4
+ data.tar.gz: 7bcdd497bcb3ebb395a43ccb54c803efb525e739d1425670f488f675bb4d4d86
5
5
  SHA512:
6
- metadata.gz: 897a6f309d17ba4f312e5b9b86a8daf4ab76050a6b5cd17543eaaf728c3ba242bc1ec34a8efb25f17395095cccf5225bf918850fb4f373ba0132311c3c0e84c7
7
- data.tar.gz: 785494e60e28e09a5b3343eaf9b2dfc0d9a70343cf8f56ddd81768b172c0753696a3b03df84f24510ba50687d9d7cb6af8a3bce72fad2331e88bc195d9dc73b5
6
+ metadata.gz: 2d68c23981b7efd4d919bda74cc2f67f15326b46d27a02103daf04bba9f76ed3de9c9b7b07de4d30d36a6a78660e9d07ac73d272767e76e765b2a8900a29d153
7
+ data.tar.gz: d50fc1f4f3fe447543cf0f7dfeff79de88082ec0d5aecf9cd602654349387a50100e6e7c4e8adcbfeb8af2f6962e060bac06d8a38a9b5362314e0e623351f241
data/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ 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.13] - 2026-06-17
11
+
12
+ ### Added
13
+
14
+ - **Cognito identity enrichment** (populate `payload['email']` on ACCESS tokens):
15
+ - Cognito **access tokens do not carry `email`** (only ID tokens do). New opt-in
16
+ enrichment fetches the user's standard identity attributes via Cognito `GetUser`
17
+ — authorized by the access token's own `aws.cognito.signin.user.admin` scope, so
18
+ **no AWS IAM credentials are required** — and merges them into the decoded payload.
19
+ - Enable with `config.enable_user_identity_enrichment = true` (env
20
+ `ENABLE_USER_IDENTITY_ENRICHMENT=true`), or pass
21
+ `enable_user_identity_enrichment: true` to `create_cognito_validator`.
22
+ - Runs only for `token_use == 'access'` tokens that don't already have `email`,
23
+ and only when `enrich_user_data` is requested (default in `validate`).
24
+ - Results are cached per `sub` (default 300s, `IDENTITY_CACHE_TIMEOUT`). Any failure
25
+ is non-fatal: the token still validates, just without identity enrichment.
26
+ - Default merged attributes: `email`, `email_verified`, `name`, `given_name`,
27
+ `family_name`, `phone_number`, `phone_number_verified`, `preferred_username`
28
+ (`*_verified` normalized to boolean). `custom:*` attributes are NOT merged.
29
+ - New `JwtAuthCognito::CognitoIdentityService` class.
30
+ - New dependency: `aws-sdk-cognitoidentityprovider`.
31
+
10
32
  ## [1.0.0-beta.11] - 2025-01-23
11
33
 
12
34
  ### 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,32 @@ 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
+ ```ruby
124
+ JwtAuthCognito.configure do |config|
125
+ config.cognito_region = 'us-east-1'
126
+ config.enable_user_identity_enrichment = true
127
+ config.identity_cache_timeout = 300 # TTL de caché por sub (segundos)
128
+ end
129
+
130
+ result = validator.validate(access_token, api_key: api_key, enrich_user_data: true)
131
+ result[:payload]['email'] # ✅ ahora disponible
132
+ ```
133
+
134
+ **Comportamiento:**
135
+ - Solo actúa sobre tokens `token_use == 'access'` que aún no traen `email`. Los ID tokens se omiten (ya lo traen).
136
+ - Solo se ejecuta cuando `enrich_user_data` está activo (default en `validate`).
137
+ - Resultados cacheados por `sub` (default 300s) para no llamar a Cognito en cada request.
138
+ - Tolerante a fallos: si `GetUser` falla, el token sigue siendo válido (solo no se enriquece).
139
+ - Los `*_verified` se normalizan a boolean. Los atributos `custom:*` **no** se fusionan.
140
+
116
141
  ## Configuración AWS para Development
117
142
 
118
143
  ### 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'
@@ -59,6 +59,24 @@ module JwtAuthCognito
59
59
  @current_user_permissions ||= fetch_current_user_permissions
60
60
  end
61
61
 
62
+ # Raises ForbiddenError unless the user has the given permission over a specific resource.
63
+ # By convention reads the resource ID from params[:id]; override with resource_id: keyword arg.
64
+ #
65
+ # before_action -> { authorize_resource_permission!('fleet:vehicles:write') }, only: [:update]
66
+ # before_action -> { authorize_resource_permission!('fleet:vehicles:write', resource_id: params[:vehicle_id]) }
67
+ def authorize_resource_permission!(permission, resource_id: params[:id])
68
+ uid = jwt_user_id
69
+ aid = jwt_app_id
70
+ oid = jwt_org_id
71
+
72
+ raise JwtAuthCognito::ForbiddenError, 'Access denied' unless uid && aid && oid && resource_id
73
+
74
+ return if jwt_validator.check_resource_permission?(uid, aid, oid, resource_id.to_s, permission)
75
+
76
+ log_resource_permission_denied(permission, resource_id)
77
+ raise JwtAuthCognito::ForbiddenError, 'Access denied'
78
+ end
79
+
62
80
  private
63
81
 
64
82
  def fetch_current_user_permissions
@@ -114,5 +132,15 @@ module JwtAuthCognito
114
132
  "app_id=#{jwt_app_id} org_id=#{jwt_org_id}"
115
133
  )
116
134
  end
135
+
136
+ def log_resource_permission_denied(permission, resource_id)
137
+ return unless defined?(Rails)
138
+
139
+ Rails.logger.warn(
140
+ "[RESOURCE_PERMISSION_DENIED] user_id=#{jwt_user_id} " \
141
+ "permission=#{permission} resource_id=#{resource_id} " \
142
+ "app_id=#{jwt_app_id} org_id=#{jwt_org_id}"
143
+ )
144
+ end
117
145
  end
118
146
  end
@@ -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
 
@@ -270,6 +283,28 @@ module JwtAuthCognito
270
283
  @user_data_service.resolve_effective_permissions(user_id, app_id, org_id)
271
284
  end
272
285
 
286
+ # Get the permissions the user has over a specific resource instance (ReBAC).
287
+ # Returns { resource_type:, resource_id:, permissions:, mode: } or nil.
288
+ def get_resource_permissions(user_id, app_id, org_id, resource_type, resource_id)
289
+ return nil unless @user_data_service
290
+
291
+ result = @user_data_service.resolve_resource_permissions(user_id, app_id, org_id, resource_id)
292
+ return nil unless result
293
+
294
+ { resource_type: resource_type, resource_id: resource_id,
295
+ permissions: result[:permissions], mode: result[:mode] }
296
+ end
297
+
298
+ # Verify if the user has a specific permission over a concrete resource instance.
299
+ def check_resource_permission?(user_id, app_id, org_id, resource_id, permission)
300
+ return false unless @user_data_service
301
+
302
+ result = @user_data_service.resolve_resource_permissions(user_id, app_id, org_id, resource_id)
303
+ return false unless result
304
+
305
+ PermissionChecker.permission_in_list?(permission, result[:permissions])
306
+ end
307
+
273
308
  def is_token_expired?(token)
274
309
  payload = decode_token(token)
275
310
  return true if payload.is_a?(Hash) && payload[:error]
@@ -423,5 +458,15 @@ module JwtAuthCognito
423
458
  { valid: true } # Continue with basic validation if user data service fails
424
459
  end
425
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
426
471
  end
427
472
  end
@@ -372,6 +372,35 @@ module JwtAuthCognito
372
372
  @cache_timestamps[key] = Time.now.to_i
373
373
  end
374
374
 
375
+ # Resolve permissions the user has over a specific resource instance (ReBAC).
376
+ # Applies resourceRestrictions per-permission: open mode if none configured.
377
+ # Returns nil if the user has no active membership in that org.
378
+ def resolve_resource_permissions(user_id, app_id, organization_id, resource_id)
379
+ effective = resolve_effective_permissions(user_id, app_id, organization_id)
380
+ return nil unless effective
381
+
382
+ raw = @redis_service.get("user:permissions:#{user_id}")
383
+ return nil unless raw
384
+
385
+ data = JSON.parse(raw)
386
+ restrictions = data.dig('permissions', app_id, organization_id, 'resourceRestrictions')
387
+ has_any_restriction = restrictions.is_a?(Hash) && !restrictions.empty?
388
+ mode = has_any_restriction ? 'restricted' : 'open'
389
+
390
+ allowed = effective.select do |perm|
391
+ if !restrictions.is_a?(Hash) || !restrictions.key?(perm)
392
+ true
393
+ else
394
+ restrictions[perm].is_a?(Array) && restrictions[perm].include?(resource_id)
395
+ end
396
+ end
397
+
398
+ { permissions: allowed, mode: mode }
399
+ rescue StandardError => e
400
+ puts "Error resolving resource permissions for #{user_id}:#{resource_id}: #{e.message}"
401
+ nil
402
+ end
403
+
375
404
  def compute_permissions_from_roles(app_id, organization_id, role_names)
376
405
  roles_data = get_app_roles(app_id, organization_id)
377
406
  return [] unless roles_data.is_a?(Hash)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JwtAuthCognito
4
- VERSION = '1.0.0-beta.11'
4
+ VERSION = '1.0.0-beta.13'
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'
@@ -42,7 +43,8 @@ module JwtAuthCognito
42
43
 
43
44
  # Convenience factory method to create a Cognito validator
44
45
  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)
46
+ enable_api_key_validation: false, enable_user_data_retrieval: false,
47
+ enable_user_identity_enrichment: false)
46
48
  old_config = configuration.dup
47
49
 
48
50
  configure do |config|
@@ -52,6 +54,7 @@ module JwtAuthCognito
52
54
  config.cognito_client_secret = client_secret if client_secret
53
55
  config.enable_api_key_validation = enable_api_key_validation
54
56
  config.enable_user_data_retrieval = enable_user_data_retrieval
57
+ config.enable_user_identity_enrichment = enable_user_identity_enrichment
55
58
 
56
59
  # Apply Redis configuration
57
60
  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.11
4
+ version: 1.0.0.pre.beta.13
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-02 00:00:00.000000000 Z
11
+ date: 2026-06-17 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