jwt_auth_cognito 0.1.0 → 0.3.0

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.
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+ require 'aws-sdk-ssm'
7
+
8
+ module JwtAuthCognito
9
+ class SSMService
10
+ class << self
11
+ attr_accessor :client, :certificate_cache
12
+ end
13
+
14
+ @client = nil
15
+ @certificate_cache = {}
16
+
17
+ # Initialize the SSM client
18
+ def self.get_client
19
+ @client ||= begin
20
+ require 'aws-sdk-ssm'
21
+ region = ENV['AWS_REGION'] || ENV['AWS_DEFAULT_REGION'] || 'us-east-1'
22
+ Aws::SSM::Client.new(region: region)
23
+ end
24
+ rescue LoadError
25
+ raise ConfigurationError,
26
+ "aws-sdk-ssm gem is required for SSM functionality. Add 'gem \"aws-sdk-ssm\"' to your Gemfile"
27
+ end
28
+
29
+ # Gets a certificate from AWS Parameter Store (compatible with auth-service)
30
+ # Uses the same path pattern: /${cert_path}/${cert_name}
31
+ def self.get_ca_certificate(cert_path, cert_name)
32
+ full_path = "/#{cert_path}/#{cert_name}"
33
+
34
+ # Check cache first
35
+ if @certificate_cache.key?(full_path)
36
+ puts '📋 Using cached certificate from SSM'
37
+ return @certificate_cache[full_path]
38
+ end
39
+
40
+ begin
41
+ puts "📡 Getting certificate from Parameter Store: #{full_path}"
42
+
43
+ client = get_client
44
+ response = client.get_parameter({
45
+ name: full_path,
46
+ with_decryption: true
47
+ })
48
+
49
+ raise ConfigurationError, "Certificate parameter not found or invalid: #{full_path}" unless response.parameter&.value
50
+
51
+ # Cache the certificate
52
+ @certificate_cache[full_path] = response.parameter.value
53
+ puts '✅ Certificate obtained from SSM and cached'
54
+
55
+ response.parameter.value
56
+ rescue Aws::SSM::Errors::ParameterNotFound
57
+ raise ConfigurationError, "Certificate parameter not found: #{full_path}"
58
+ rescue Aws::SSM::Errors::ServiceError => e
59
+ puts "❌ Error getting certificate from SSM (#{full_path}): #{e.message}"
60
+ raise ConfigurationError, "Error accessing SSM: #{e.message}"
61
+ rescue StandardError => e
62
+ puts "❌ Error getting certificate from SSM (#{full_path}): #{e.message}"
63
+ raise e
64
+ end
65
+ end
66
+
67
+ # Gets a parameter from AWS Parameter Store
68
+ def self.get_parameter(parameter_name, with_decryption = true)
69
+ # Check cache first
70
+ return @certificate_cache[parameter_name] if @certificate_cache.key?(parameter_name)
71
+
72
+ begin
73
+ client = get_client
74
+ response = client.get_parameter({
75
+ name: parameter_name,
76
+ with_decryption: with_decryption
77
+ })
78
+
79
+ raise ConfigurationError, "Parameter not found or invalid: #{parameter_name}" unless response.parameter&.value
80
+
81
+ # Cache the parameter
82
+ @certificate_cache[parameter_name] = response.parameter.value
83
+
84
+ response.parameter.value
85
+ rescue Aws::SSM::Errors::ParameterNotFound
86
+ raise ConfigurationError, "Parameter not found: #{parameter_name}"
87
+ rescue Aws::SSM::Errors::ServiceError => e
88
+ puts "❌ Error getting parameter from SSM (#{parameter_name}): #{e.message}"
89
+ raise ConfigurationError, "Error accessing SSM: #{e.message}"
90
+ rescue StandardError => e
91
+ puts "❌ Error getting parameter from SSM (#{parameter_name}): #{e.message}"
92
+ raise e
93
+ end
94
+ end
95
+
96
+ # Clears the certificate cache
97
+ def self.clear_cache
98
+ @certificate_cache.clear
99
+ end
100
+
101
+ # Gets cache stats
102
+ def self.cache_stats
103
+ {
104
+ size: @certificate_cache.size,
105
+ keys: @certificate_cache.keys
106
+ }
107
+ end
108
+ end
109
+ end
@@ -10,14 +10,12 @@ module JwtAuthCognito
10
10
  def add_to_blacklist(token, user_id: nil)
11
11
  token_id = @redis_service.generate_token_id(token)
12
12
  ttl = calculate_ttl(token)
13
-
13
+
14
14
  result = @redis_service.save_revoked_token(token_id, ttl)
15
-
15
+
16
16
  # Track token for user if provided
17
- if user_id
18
- @redis_service.track_user_token(user_id, token_id, ttl)
19
- end
20
-
17
+ @redis_service.track_user_token(user_id, token_id, ttl) if user_id
18
+
21
19
  result
22
20
  end
23
21
 
@@ -39,18 +37,18 @@ module JwtAuthCognito
39
37
  def calculate_ttl(token)
40
38
  begin
41
39
  payload = JWT.decode(token, nil, false).first
42
- exp = payload["exp"]
43
-
40
+ exp = payload['exp']
41
+
44
42
  if exp
45
43
  ttl = exp - Time.now.to_i
46
- return ttl > 0 ? ttl : 1 # At least 1 second TTL
44
+ return ttl.positive? ? ttl : 1 # At least 1 second TTL
47
45
  end
48
46
  rescue JWT::DecodeError
49
47
  # If we can't decode the token, use a default TTL
50
48
  end
51
-
49
+
52
50
  # Default TTL of 1 day if we can't determine expiration
53
- 86400
51
+ 86_400
54
52
  end
55
53
  end
56
- end
54
+ end
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module JwtAuthCognito
6
+ class UserDataService
7
+ DEFAULT_CACHE_TTL = 300 # 5 minutes
8
+
9
+ def initialize(redis_service = nil, config = {})
10
+ @redis_service = redis_service || RedisService.new
11
+ @config = {
12
+ enable_user_data_retrieval: true,
13
+ include_applications: true,
14
+ include_organizations: true,
15
+ include_roles: true,
16
+ include_effective_permissions: false,
17
+ cache_timeout: DEFAULT_CACHE_TTL
18
+ }.merge(config)
19
+
20
+ @cache = {}
21
+ @cache_timestamps = {}
22
+ @stats = {
23
+ service: 'UserDataService',
24
+ connection_status: 'not-initialized',
25
+ initialized: false,
26
+ cache_hits: 0,
27
+ cache_misses: 0
28
+ }
29
+ end
30
+
31
+ def initialize!
32
+ @redis_service.initialize! unless @redis_service.connected?
33
+ @stats[:initialized] = true
34
+ @stats[:connection_status] = 'connected'
35
+ puts '✅ UserDataService initialized successfully'
36
+ rescue StandardError => e
37
+ @stats[:initialized] = false
38
+ @stats[:connection_status] = 'error'
39
+ @stats[:error] = e.message
40
+ puts "❌ UserDataService initialization failed: #{e.message}"
41
+ raise e
42
+ end
43
+
44
+ def get_user_permissions(user_id)
45
+ return nil unless @config[:enable_user_data_retrieval]
46
+
47
+ cache_key = "user_permissions:#{user_id}"
48
+
49
+ # Check cache first
50
+ cached_data = get_from_cache(cache_key)
51
+ if cached_data
52
+ @stats[:cache_hits] += 1
53
+ return cached_data
54
+ end
55
+
56
+ @stats[:cache_misses] += 1
57
+
58
+ begin
59
+ redis_key = "user:permissions:#{user_id}"
60
+ data = @redis_service.get(redis_key)
61
+
62
+ return nil unless data
63
+
64
+ permissions = JSON.parse(data)
65
+ set_in_cache(cache_key, permissions)
66
+
67
+ permissions
68
+ rescue StandardError => e
69
+ puts "Error fetching user permissions for #{user_id}: #{e.message}"
70
+ nil
71
+ end
72
+ end
73
+
74
+ def get_application(app_id)
75
+ return nil unless @config[:include_applications]
76
+
77
+ cache_key = "app:#{app_id}"
78
+
79
+ # Check cache first
80
+ cached_data = get_from_cache(cache_key)
81
+ if cached_data
82
+ @stats[:cache_hits] += 1
83
+ return cached_data
84
+ end
85
+
86
+ @stats[:cache_misses] += 1
87
+
88
+ begin
89
+ redis_key = "app:#{app_id}"
90
+ data = @redis_service.get(redis_key)
91
+
92
+ return nil unless data
93
+
94
+ application = JSON.parse(data)
95
+ set_in_cache(cache_key, application)
96
+
97
+ application
98
+ rescue StandardError => e
99
+ puts "Error fetching application #{app_id}: #{e.message}"
100
+ nil
101
+ end
102
+ end
103
+
104
+ def get_organization(app_id, organization_id)
105
+ return nil unless @config[:include_organizations]
106
+
107
+ cache_key = "org:#{app_id}:#{organization_id}"
108
+
109
+ # Check cache first
110
+ cached_data = get_from_cache(cache_key)
111
+ if cached_data
112
+ @stats[:cache_hits] += 1
113
+ return cached_data
114
+ end
115
+
116
+ @stats[:cache_misses] += 1
117
+
118
+ begin
119
+ redis_key = "org:#{app_id}:#{organization_id}"
120
+ data = @redis_service.get(redis_key)
121
+
122
+ return nil unless data
123
+
124
+ organization = JSON.parse(data)
125
+ set_in_cache(cache_key, organization)
126
+
127
+ organization
128
+ rescue StandardError => e
129
+ puts "Error fetching organization #{app_id}:#{organization_id}: #{e.message}"
130
+ nil
131
+ end
132
+ end
133
+
134
+ def get_app_roles(app_id, organization_id)
135
+ return nil unless @config[:include_roles]
136
+
137
+ cache_key = "app_roles:#{app_id}:#{organization_id}"
138
+
139
+ # Check cache first
140
+ cached_data = get_from_cache(cache_key)
141
+ if cached_data
142
+ @stats[:cache_hits] += 1
143
+ return cached_data
144
+ end
145
+
146
+ @stats[:cache_misses] += 1
147
+
148
+ begin
149
+ redis_key = "app:roles:#{app_id}:#{organization_id}"
150
+ data = @redis_service.get(redis_key)
151
+
152
+ return nil unless data
153
+
154
+ roles = JSON.parse(data)
155
+ set_in_cache(cache_key, roles)
156
+
157
+ roles
158
+ rescue StandardError => e
159
+ puts "Error fetching app roles #{app_id}:#{organization_id}: #{e.message}"
160
+ nil
161
+ end
162
+ end
163
+
164
+ def get_app_schema(app_id)
165
+ cache_key = "app_schema:#{app_id}"
166
+
167
+ # Check cache first
168
+ cached_data = get_from_cache(cache_key)
169
+ if cached_data
170
+ @stats[:cache_hits] += 1
171
+ return cached_data
172
+ end
173
+
174
+ @stats[:cache_misses] += 1
175
+
176
+ begin
177
+ redis_key = 'app-schemas'
178
+ data = @redis_service.get(redis_key)
179
+
180
+ return nil unless data
181
+
182
+ schemas = JSON.parse(data)
183
+ schema = schemas[app_id]
184
+
185
+ return nil unless schema
186
+
187
+ set_in_cache(cache_key, schema)
188
+ schema
189
+ rescue StandardError => e
190
+ puts "Error fetching app schema for #{app_id}: #{e.message}"
191
+ nil
192
+ end
193
+ end
194
+
195
+ def get_effective_permissions(user_id, app_id, organization_id)
196
+ return nil unless @config[:include_effective_permissions]
197
+
198
+ cache_key = "effective_permissions:#{user_id}:#{app_id}:#{organization_id}"
199
+
200
+ # Check cache first
201
+ cached_data = get_from_cache(cache_key)
202
+ if cached_data
203
+ @stats[:cache_hits] += 1
204
+ return cached_data
205
+ end
206
+
207
+ @stats[:cache_misses] += 1
208
+
209
+ begin
210
+ redis_key = "permissions:cache:#{user_id}:#{app_id}:#{organization_id}"
211
+ data = @redis_service.get(redis_key)
212
+
213
+ return nil unless data
214
+
215
+ effective_permissions = JSON.parse(data)
216
+
217
+ # Cache with shorter TTL for permission cache
218
+ set_in_cache(cache_key, effective_permissions, (@config[:cache_timeout] / 2).to_i)
219
+
220
+ effective_permissions
221
+ rescue StandardError => e
222
+ puts "Error fetching effective permissions #{user_id}:#{app_id}:#{organization_id}: #{e.message}"
223
+ nil
224
+ end
225
+ end
226
+
227
+ def get_user_organizations(user_id)
228
+ permissions = get_user_permissions(user_id)
229
+ return [] unless permissions&.dig('permissions')
230
+
231
+ user_organizations = []
232
+
233
+ permissions['permissions'].each do |app_id, orgs|
234
+ orgs.each do |organization_id, org_data|
235
+ next unless org_data['status'] == 'active'
236
+
237
+ effective_permissions = nil
238
+ effective_permissions = get_effective_permissions(user_id, app_id, organization_id) if @config[:include_effective_permissions]
239
+
240
+ user_org = {
241
+ 'appId' => app_id,
242
+ 'organizationId' => organization_id,
243
+ 'roles' => org_data['roles'],
244
+ 'status' => org_data['status']
245
+ }
246
+
247
+ user_org['effectivePermissions'] = effective_permissions if effective_permissions
248
+
249
+ user_organizations << user_org
250
+ end
251
+ end
252
+
253
+ user_organizations
254
+ end
255
+
256
+ def get_user_applications(user_id)
257
+ user_organizations = get_user_organizations(user_id)
258
+ unique_app_ids = user_organizations.map { |org| org['appId'] }.uniq
259
+
260
+ applications = []
261
+ unique_app_ids.each do |app_id|
262
+ app = get_application(app_id)
263
+ applications << app if app && app['isActive']
264
+ end
265
+
266
+ applications
267
+ end
268
+
269
+ def get_comprehensive_user_data(user_id)
270
+ permissions = get_user_permissions(user_id)
271
+ organizations = get_user_organizations(user_id)
272
+ applications = get_user_applications(user_id)
273
+
274
+ {
275
+ 'permissions' => permissions,
276
+ 'organizations' => organizations,
277
+ 'applications' => applications
278
+ }
279
+ end
280
+
281
+ def clear_user_cache(user_id)
282
+ keys_to_remove = @cache.keys.select { |key| key.include?(user_id) }
283
+ keys_to_remove.each do |key|
284
+ @cache.delete(key)
285
+ @cache_timestamps.delete(key)
286
+ end
287
+ end
288
+
289
+ def clear_all_cache
290
+ @cache.clear
291
+ @cache_timestamps.clear
292
+ end
293
+
294
+ def stats
295
+ @stats.dup
296
+ end
297
+
298
+ def initialized?
299
+ @stats[:initialized] && @redis_service.connected?
300
+ end
301
+
302
+ def shutdown
303
+ clear_all_cache
304
+ @redis_service.disconnect if @redis_service.respond_to?(:disconnect)
305
+ @stats[:initialized] = false
306
+ @stats[:connection_status] = 'disconnected'
307
+ end
308
+
309
+ private
310
+
311
+ def get_from_cache(key)
312
+ return nil unless @cache.key?(key)
313
+
314
+ timestamp = @cache_timestamps[key]
315
+ return nil unless timestamp
316
+
317
+ # Check if cache entry has expired
318
+ if Time.now.to_i - timestamp > @config[:cache_timeout]
319
+ @cache.delete(key)
320
+ @cache_timestamps.delete(key)
321
+ return nil
322
+ end
323
+
324
+ @cache[key]
325
+ end
326
+
327
+ def set_in_cache(key, value, _ttl = nil)
328
+ @cache[key] = value
329
+ @cache_timestamps[key] = Time.now.to_i
330
+ end
331
+ end
332
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JwtAuthCognito
4
- VERSION = "0.1.0"
5
- end
4
+ VERSION = '0.3.0'
5
+ end
@@ -1,18 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "jwt_auth_cognito/version"
4
- require_relative "jwt_auth_cognito/configuration"
5
- require_relative "jwt_auth_cognito/jwks_service"
6
- require_relative "jwt_auth_cognito/redis_service"
7
- require_relative "jwt_auth_cognito/token_blacklist_service"
8
- require_relative "jwt_auth_cognito/jwt_validator"
3
+ require_relative 'jwt_auth_cognito/version'
4
+ require_relative 'jwt_auth_cognito/configuration'
5
+ require_relative 'jwt_auth_cognito/ssm_service'
6
+ require_relative 'jwt_auth_cognito/jwks_service'
7
+ require_relative 'jwt_auth_cognito/redis_service'
8
+ require_relative 'jwt_auth_cognito/token_blacklist_service'
9
+ require_relative 'jwt_auth_cognito/api_key_validator'
10
+ require_relative 'jwt_auth_cognito/user_data_service'
11
+ require_relative 'jwt_auth_cognito/error_utils'
12
+ require_relative 'jwt_auth_cognito/jwt_validator'
9
13
 
10
14
  module JwtAuthCognito
11
15
  class Error < StandardError; end
12
16
  class ValidationError < Error; end
13
17
  class BlacklistError < Error; end
14
18
  class ConfigurationError < Error; end
15
-
19
+
16
20
  # Specific JWT error types (matching Node.js implementation)
17
21
  class TokenExpiredError < ValidationError; end
18
22
  class TokenNotActiveError < ValidationError; end
@@ -33,9 +37,37 @@ module JwtAuthCognito
33
37
  def self.reset_configuration!
34
38
  @configuration = Configuration.new
35
39
  end
40
+
41
+ # Convenience factory method to create a Cognito validator
42
+ def self.create_cognito_validator(region:, user_pool_id:, client_id: nil, client_secret: nil, redis_config: {},
43
+ enable_api_key_validation: false, enable_user_data_retrieval: false)
44
+ old_config = configuration.dup
45
+
46
+ configure do |config|
47
+ config.cognito_region = region
48
+ config.cognito_user_pool_id = user_pool_id
49
+ config.cognito_client_id = client_id if client_id
50
+ config.cognito_client_secret = client_secret if client_secret
51
+ config.enable_api_key_validation = enable_api_key_validation
52
+ config.enable_user_data_retrieval = enable_user_data_retrieval
53
+
54
+ # Apply Redis configuration
55
+ config.redis_host = redis_config[:host] if redis_config[:host]
56
+ config.redis_port = redis_config[:port] if redis_config[:port]
57
+ config.redis_password = redis_config[:password] if redis_config[:password]
58
+ config.redis_ssl = redis_config[:tls] if redis_config.key?(:tls)
59
+ config.redis_ca_cert_path = redis_config[:ca_cert_path] if redis_config[:ca_cert_path]
60
+ config.redis_ca_cert_name = redis_config[:ca_cert_name] if redis_config[:ca_cert_name]
61
+ end
62
+
63
+ validator = JwtValidator.new
64
+
65
+ # Restore original configuration
66
+ @configuration = old_config
67
+
68
+ validator
69
+ end
36
70
  end
37
71
 
38
72
  # Cargar tareas Rake si estamos en Rails
39
- if defined?(Rails::Railtie)
40
- require_relative "jwt_auth_cognito/railtie"
41
- end
73
+ require_relative 'jwt_auth_cognito/railtie' if defined?(Rails::Railtie)