jwt_auth_cognito 0.1.1 → 1.0.0.pre.beta.1

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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "jwt"
3
+ require 'jwt'
4
4
 
5
5
  module JwtAuthCognito
6
6
  class JwtValidator
@@ -8,44 +8,221 @@ module JwtAuthCognito
8
8
  @config = config
9
9
  @jwks_service = JwksService.new(config)
10
10
  @blacklist_service = TokenBlacklistService.new(config)
11
+ @api_key_validator = config.enable_api_key_validation ? ApiKeyValidator.new(config) : nil
12
+ @user_data_service = config.enable_user_data_retrieval ? UserDataService.new(nil, config.user_data_config) : nil
13
+ @initialized = false
11
14
  end
12
15
 
13
- def validate_token(token, options = {})
16
+ def initialize!
17
+ return if @initialized
18
+
19
+ begin
20
+ @jwks_service.initialize!
21
+ @blacklist_service.initialize! if @blacklist_service.respond_to?(:initialize!)
22
+ @user_data_service&.initialize!
23
+ @initialized = true
24
+ rescue StandardError => e
25
+ ErrorUtils.log_error(e, 'JWT Validator initialization failed')
26
+ raise JwtAuthCognito::ConfigurationError, ErrorUtils::JWT_ERROR_MESSAGES['INITIALIZATION_FAILED']
27
+ end
28
+ end
29
+
30
+ # ========== 🚀 MAIN VALIDATION METHOD ==========
31
+
32
+ # Main validation method - use this for most cases
33
+ # Intelligently validates tokens with all features:
34
+ # - JWT validation (basic or secure)
35
+ # - API key validation (if provided)
36
+ # - Blacklist checking
37
+ # - Automatic appId verification
38
+ # - User data enrichment (if enabled)
39
+ def validate(token, options = {})
14
40
  @config.validate!
15
-
16
- # Check blacklist first
17
- if @blacklist_service.is_blacklisted?(token)
18
- return { valid: false, error: "Token has been revoked" }
41
+
42
+ api_key = options[:api_key]
43
+ force_secure = options[:force_secure] || false
44
+ enrich_user_data = options.fetch(:enrich_user_data, true)
45
+ require_app_access = options[:require_app_access] || false
46
+
47
+ # Step 1: Validate API key if provided
48
+ api_key_data = nil
49
+ if api_key && @config.enable_api_key_validation && @api_key_validator
50
+ api_key_result = @api_key_validator.validate_api_key(api_key)
51
+ return { valid: false, error: api_key_result[:error] || 'API key validation failed' } unless api_key_result[:valid]
52
+
53
+ api_key_data = api_key_result[:key_data]
19
54
  end
20
55
 
21
- # Choose validation method based on configuration
22
- case @config.validation_mode
23
- when :secure
24
- validate_token_secure(token, options)
25
- when :basic
26
- validate_token_basic(token, options)
27
- else
28
- raise ConfigurationError, "Invalid validation_mode: #{@config.validation_mode}"
56
+ # Step 2: Check blacklist first
57
+ return { valid: false, error: 'Token has been revoked' } if @blacklist_service.is_blacklisted?(token)
58
+
59
+ # Step 3: Validate JWT token
60
+ validation_mode = force_secure ? :secure : @config.validation_mode
61
+ token_result = case validation_mode
62
+ when :secure
63
+ validate_token_secure(token, options)
64
+ when :basic
65
+ validate_token_basic(token, options)
66
+ else
67
+ raise ConfigurationError, "Invalid validation_mode: #{validation_mode}"
68
+ end
69
+
70
+ return token_result unless token_result[:valid] && token_result[:payload]
71
+
72
+ # Step 4: Verify appId access if API key has one
73
+ if api_key_data
74
+ app_validation = verify_app_access(api_key_data, token_result[:payload], require_app_access)
75
+ return app_validation unless app_validation[:valid]
76
+ end
77
+
78
+ # Step 5: Enrich with user data if requested
79
+ enriched_result = token_result.dup
80
+ enriched_result[:api_key] = api_key_data if api_key_data
81
+
82
+ if enrich_user_data && @config.enable_user_data_retrieval && @user_data_service
83
+ user_id = token_result[:payload]['sub']
84
+ if user_id
85
+ begin
86
+ user_data = @user_data_service.get_comprehensive_user_data(user_id)
87
+ enriched_result[:user_permissions] = user_data['permissions']
88
+ enriched_result[:user_organizations] = user_data['organizations']
89
+ enriched_result[:applications] = user_data['applications']
90
+ rescue StandardError => e
91
+ ErrorUtils.log_error(e, 'User data retrieval failed')
92
+ # Continue with basic validation if user data service fails
93
+ end
94
+ end
95
+ end
96
+
97
+ enriched_result
98
+ end
99
+
100
+ # ========== SIMPLIFIED PUBLIC API ==========
101
+
102
+ # Quick validation for simple use cases
103
+ # Just validates the JWT token (includes blacklist check)
104
+ def validate_token(token, options = {})
105
+ result = validate(token, options.merge(enrich_user_data: false))
106
+ {
107
+ valid: result[:valid],
108
+ payload: result[:payload],
109
+ sub: result[:sub],
110
+ username: result[:username],
111
+ token_use: result[:token_use],
112
+ error: result[:error]
113
+ }
114
+ end
115
+
116
+ # Validate with API key (automatic appId verification)
117
+ # Use this when you have an API key and want automatic security
118
+ def validate_with_api_key(token, api_key, options = {})
119
+ validate(token, options.merge(api_key: api_key))
120
+ end
121
+
122
+ # Validate with strict appId requirement
123
+ # Use this when you MUST ensure user has access to a specific app
124
+ def validate_with_app_access(token, api_key, options = {})
125
+ validate(token, options.merge(api_key: api_key, require_app_access: true))
126
+ end
127
+
128
+ # Get enriched validation (user data included)
129
+ # Use this when you need user permissions, organizations, apps
130
+ def validate_enriched(token, api_key = nil, options = {})
131
+ validate(token, options.merge(api_key: api_key, enrich_user_data: true))
132
+ end
133
+
134
+ # ========== LEGACY METHODS (DEPRECATED) ==========
135
+
136
+ # @deprecated Use validate() or validate_with_api_key() instead
137
+ def validate_token_with_api_key(token, api_key = nil, options = {})
138
+ puts 'WARNING: validate_token_with_api_key is deprecated. Use validate() or validate_with_api_key() instead.'
139
+ result = validate(token, options.merge(api_key: api_key, enrich_user_data: false))
140
+ {
141
+ valid: result[:valid],
142
+ payload: result[:payload],
143
+ sub: result[:sub],
144
+ username: result[:username],
145
+ token_use: result[:token_use],
146
+ api_key: result[:api_key],
147
+ error: result[:error]
148
+ }
149
+ end
150
+
151
+ # @deprecated Use validate_with_app_access() instead
152
+ def validate_token_with_app_id(token, api_key, options = {})
153
+ puts 'WARNING: validate_token_with_app_id is deprecated. Use validate_with_app_access() instead.'
154
+ validate_with_app_access(token, api_key, options.merge(enrich_user_data: false))
155
+ end
156
+
157
+ # @deprecated Use validate() instead
158
+ def validate_token_enhanced(token, api_key = nil, options = {})
159
+ puts 'WARNING: validate_token_enhanced is deprecated. Use validate() instead.'
160
+ result = validate(token, options.merge(api_key: api_key, enrich_user_data: false))
161
+ {
162
+ valid: result[:valid],
163
+ payload: result[:payload],
164
+ sub: result[:sub],
165
+ username: result[:username],
166
+ token_use: result[:token_use],
167
+ api_key: result[:api_key],
168
+ error: result[:error]
169
+ }
170
+ end
171
+
172
+ # @deprecated Use validate_enriched() instead
173
+ def validate_token_enriched(token, api_key = nil, options = {})
174
+ puts 'WARNING: validate_token_enriched is deprecated. Use validate_enriched() instead.'
175
+ validate_enriched(token, api_key, options)
176
+ end
177
+
178
+ def old_validate_token_enriched(token, api_key = nil, options = {})
179
+ # First, perform standard token validation
180
+ basic_result = validate_token_with_api_key(token, api_key, options)
181
+
182
+ # If basic validation fails, return early
183
+ return basic_result unless basic_result[:valid] && basic_result[:payload]
184
+
185
+ # If user data retrieval is not enabled, return basic result
186
+ return basic_result unless @config.enable_user_data_retrieval && @user_data_service
187
+
188
+ # Extract user ID from the token
189
+ user_id = basic_result[:payload]['sub']
190
+ unless user_id
191
+ puts 'Token does not contain sub claim, cannot retrieve user data'
192
+ return basic_result
193
+ end
194
+
195
+ begin
196
+ # Get comprehensive user data from Redis
197
+ user_data = @user_data_service.get_comprehensive_user_data(user_id)
198
+
199
+ # Add user data to the result
200
+ enriched_result = basic_result.dup
201
+ enriched_result[:user_permissions] = user_data['permissions']
202
+ enriched_result[:user_organizations] = user_data['organizations']
203
+ enriched_result[:applications] = user_data['applications']
204
+
205
+ enriched_result
206
+ rescue StandardError => e
207
+ ErrorUtils.log_error(e, 'User data retrieval failed')
208
+ # Return basic result even if user data retrieval fails
209
+ basic_result
29
210
  end
30
211
  end
31
212
 
32
213
  def validate_access_token(token)
33
214
  result = validate_token(token)
34
-
35
- if result[:valid] && result[:payload]["token_use"] != "access"
36
- return { valid: false, error: "Token is not an access token" }
37
- end
38
-
215
+
216
+ return { valid: false, error: 'Token is not an access token' } if result[:valid] && result[:payload]['token_use'] != 'access'
217
+
39
218
  result
40
219
  end
41
220
 
42
221
  def validate_id_token(token)
43
222
  result = validate_token(token)
44
-
45
- if result[:valid] && result[:payload]["token_use"] != "id"
46
- return { valid: false, error: "Token is not an ID token" }
47
- end
48
-
223
+
224
+ return { valid: false, error: 'Token is not an ID token' } if result[:valid] && result[:payload]['token_use'] != 'id'
225
+
49
226
  result
50
227
  end
51
228
 
@@ -64,11 +241,34 @@ module JwtAuthCognito
64
241
  # Utility methods inspired by Node.js package
65
242
  def extract_token_from_header(authorization_header)
66
243
  return nil unless authorization_header
67
-
244
+
68
245
  match = authorization_header.match(/\ABearer (.+)\z/)
69
246
  match ? match[1] : nil
70
247
  end
71
248
 
249
+ def extract_api_key_from_header(api_key_header)
250
+ # Support common API key header formats
251
+ return nil unless api_key_header
252
+
253
+ api_key_header.strip
254
+ end
255
+
256
+ def extract_api_key_from_headers(headers)
257
+ # Check various common API key header names (case insensitive)
258
+ api_key_headers = %w[x-api-key X-API-Key X-API-KEY X-Api-Key]
259
+
260
+ api_key_headers.each do |header_name|
261
+ # Convert headers to a case-insensitive hash for lookup
262
+ header_key = headers.keys.find { |key| key.downcase == header_name.downcase }
263
+ next unless header_key
264
+
265
+ value = headers[header_key]
266
+ return extract_api_key_from_header(value) if value
267
+ end
268
+
269
+ nil
270
+ end
271
+
72
272
  def decode_token(token)
73
273
  JWT.decode(token, nil, false).first
74
274
  rescue JWT::DecodeError => e
@@ -80,15 +280,15 @@ module JwtAuthCognito
80
280
  return payload if payload.is_a?(Hash) && payload[:error]
81
281
 
82
282
  {
83
- sub: payload["sub"],
84
- username: payload["cognito:username"] || payload["username"],
85
- email: payload["email"],
86
- token_use: payload["token_use"],
87
- client_id: payload["aud"],
88
- issued_at: payload["iat"] ? Time.at(payload["iat"]) : nil,
89
- expires_at: payload["exp"] ? Time.at(payload["exp"]) : nil,
90
- not_before: payload["nbf"] ? Time.at(payload["nbf"]) : nil,
91
- jti: payload["jti"],
283
+ sub: payload['sub'],
284
+ username: payload['cognito:username'] || payload['username'],
285
+ email: payload['email'],
286
+ token_use: payload['token_use'],
287
+ client_id: payload['aud'],
288
+ issued_at: payload['iat'] ? Time.at(payload['iat']) : nil,
289
+ expires_at: payload['exp'] ? Time.at(payload['exp']) : nil,
290
+ not_before: payload['nbf'] ? Time.at(payload['nbf']) : nil,
291
+ jti: payload['jti'],
92
292
  has_client_secret: @config.has_client_secret?
93
293
  }
94
294
  end
@@ -107,7 +307,7 @@ module JwtAuthCognito
107
307
  payload = decode_token(token)
108
308
  return true if payload.is_a?(Hash) && payload[:error]
109
309
 
110
- exp = payload["exp"]
310
+ exp = payload['exp']
111
311
  return false unless exp
112
312
 
113
313
  Time.now.to_i >= exp
@@ -117,18 +317,18 @@ module JwtAuthCognito
117
317
  payload = decode_token(token)
118
318
  return nil if payload.is_a?(Hash) && payload[:error]
119
319
 
120
- exp = payload["exp"]
320
+ exp = payload['exp']
121
321
  return nil unless exp
122
322
 
123
323
  seconds = exp - Time.now.to_i
124
- seconds > 0 ? seconds : 0
324
+ seconds.positive? ? seconds : 0
125
325
  end
126
326
 
127
327
  # Create a convenience factory method
128
328
  def self.create_cognito_validator(config = nil)
129
329
  if config
130
330
  old_config = JwtAuthCognito.configuration
131
- JwtAuthCognito.configure { |c| c = config }
331
+ JwtAuthCognito.configure { |_c| config }
132
332
  validator = new
133
333
  JwtAuthCognito.instance_variable_set(:@configuration, old_config)
134
334
  validator
@@ -142,84 +342,111 @@ module JwtAuthCognito
142
342
  def validate_token_secure(token, options = {})
143
343
  # Use JWKS validation for production
144
344
  result = @jwks_service.validate_token_with_jwks(token)
145
-
345
+
146
346
  if result[:valid]
147
347
  # Additional custom validations
148
348
  validate_custom_claims(result[:payload], options)
149
349
  end
150
-
350
+
151
351
  result
152
352
  end
153
353
 
154
354
  def validate_token_basic(token, options = {})
155
355
  # Basic validation without signature verification (development only)
156
- begin
157
- payload, header = JWT.decode(token, nil, false)
158
-
159
- # Basic claim validation
160
- validate_basic_claims(payload)
161
- validate_custom_claims(payload, options)
162
-
163
- {
164
- valid: true,
165
- payload: payload,
166
- sub: payload["sub"],
167
- username: payload["cognito:username"] || payload["username"],
168
- token_use: payload["token_use"]
169
- }
170
- rescue JWT::DecodeError => e
171
- { valid: false, error: "JWT decode error: #{e.message}" }
172
- rescue ValidationError => e
173
- { valid: false, error: e.message }
174
- rescue StandardError => e
175
- { valid: false, error: "Validation error: #{e.message}" }
176
- end
356
+
357
+ payload, = JWT.decode(token, nil, false)
358
+
359
+ # Basic claim validation
360
+ validate_basic_claims(payload)
361
+ validate_custom_claims(payload, options)
362
+
363
+ {
364
+ valid: true,
365
+ payload: payload,
366
+ sub: payload['sub'],
367
+ username: payload['cognito:username'] || payload['username'],
368
+ token_use: payload['token_use']
369
+ }
370
+ rescue JWT::DecodeError => e
371
+ { valid: false, error: "JWT decode error: #{e.message}" }
372
+ rescue ValidationError => e
373
+ { valid: false, error: e.message }
374
+ rescue StandardError => e
375
+ { valid: false, error: "Validation error: #{e.message}" }
177
376
  end
178
377
 
179
378
  def validate_basic_claims(payload)
180
379
  now = Time.now.to_i
181
-
380
+
182
381
  # Check expiration
183
- raise ValidationError, "Token has expired" if payload["exp"] && payload["exp"] < now
184
-
382
+ raise ValidationError, 'Token has expired' if payload['exp'] && payload['exp'] < now
383
+
185
384
  # Check issuer
186
385
  expected_issuer = @config.cognito_issuer
187
- if payload["iss"] != expected_issuer
188
- raise ValidationError, "Invalid issuer. Expected: #{expected_issuer}, got: #{payload["iss"]}"
189
- end
190
-
386
+ raise ValidationError, "Invalid issuer. Expected: #{expected_issuer}, got: #{payload['iss']}" if payload['iss'] != expected_issuer
387
+
191
388
  # Check token use
192
- unless %w[access id].include?(payload["token_use"])
193
- raise ValidationError, "Invalid token_use claim"
194
- end
389
+ return if %w[access id].include?(payload['token_use'])
390
+
391
+ raise ValidationError, 'Invalid token_use claim'
195
392
  end
196
393
 
197
394
  def validate_custom_claims(payload, options)
198
395
  # Validate specific user ID if provided
199
- if options[:user_id] && payload["sub"] != options[:user_id]
200
- raise ValidationError, "Token subject does not match expected user ID"
201
- end
202
-
396
+ raise ValidationError, 'Token subject does not match expected user ID' if options[:user_id] && payload['sub'] != options[:user_id]
397
+
203
398
  # Validate specific client ID if provided
204
- if options[:client_id] && payload["aud"] != options[:client_id]
205
- raise ValidationError, "Token audience does not match expected client ID"
206
- end
207
-
399
+ raise ValidationError, 'Token audience does not match expected client ID' if options[:client_id] && payload['aud'] != options[:client_id]
400
+
208
401
  # Validate token type if specified
209
- if options[:token_use] && payload["token_use"] != options[:token_use]
210
- raise ValidationError, "Token use does not match expected type"
211
- end
212
-
402
+ raise ValidationError, 'Token use does not match expected type' if options[:token_use] && payload['token_use'] != options[:token_use]
403
+
213
404
  # Custom scope validation
214
- if options[:required_scopes]
215
- token_scopes = payload["scope"]&.split(" ") || []
216
- required_scopes = Array(options[:required_scopes])
217
-
218
- missing_scopes = required_scopes - token_scopes
219
- if missing_scopes.any?
220
- raise ValidationError, "Token missing required scopes: #{missing_scopes.join(", ")}"
221
- end
405
+ return unless options[:required_scopes]
406
+
407
+ token_scopes = payload['scope']&.split || []
408
+ required_scopes = Array(options[:required_scopes])
409
+
410
+ missing_scopes = required_scopes - token_scopes
411
+ return unless missing_scopes.any?
412
+
413
+ raise ValidationError, "Token missing required scopes: #{missing_scopes.join(', ')}"
414
+ end
415
+
416
+ def verify_app_access(api_key_data, payload, require_app_access)
417
+ app_id = api_key_data['appId'] || api_key_data['metadata']&.[]('appId')
418
+
419
+ return { valid: false, error: 'API key is not associated with an application' } if !app_id && require_app_access
420
+
421
+ return { valid: true } unless app_id
422
+
423
+ user_id = payload['sub']
424
+ return { valid: false, error: 'Token missing user ID (sub claim)' } unless user_id
425
+
426
+ verify_user_app_access(user_id, app_id, require_app_access)
427
+ end
428
+
429
+ def verify_user_app_access(user_id, app_id, require_app_access)
430
+ if require_app_access && (!@config.enable_user_data_retrieval || !@user_data_service)
431
+ return { valid: false,
432
+ error: 'User data service not available for application access verification' }
433
+ end
434
+
435
+ return { valid: true } unless @config.enable_user_data_retrieval && @user_data_service
436
+
437
+ begin
438
+ user_applications = @user_data_service.get_user_applications(user_id)
439
+ has_access = user_applications.any? { |app| app['appId'] == app_id }
440
+
441
+ return { valid: false, error: "User does not have access to application #{app_id}" } unless has_access
442
+
443
+ { valid: true }
444
+ rescue StandardError => e
445
+ return { valid: false, error: 'Could not verify application access' } if require_app_access
446
+
447
+ ErrorUtils.log_error(e, 'User application access verification failed')
448
+ { valid: true } # Continue with basic validation if user data service fails
222
449
  end
223
450
  end
224
451
  end
225
- end
452
+ end
@@ -3,11 +3,11 @@
3
3
  module JwtAuthCognito
4
4
  class Railtie < Rails::Railtie
5
5
  rake_tasks do
6
- load "tasks/jwt_auth_cognito.rake"
6
+ load 'tasks/jwt_auth_cognito.rake'
7
7
  end
8
8
 
9
9
  generators do
10
- require "generators/jwt_auth_cognito/install_generator"
10
+ require 'generators/jwt_auth_cognito/install_generator'
11
11
  end
12
12
  end
13
- end
13
+ end