omniauth_openid_federation 1.3.0 → 2.0.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -1
  3. data/README.md +120 -14
  4. data/app/controllers/omniauth_openid_federation/federation_controller.rb +3 -3
  5. data/config/polyrun_coverage.yml +15 -0
  6. data/config/polyrun_spec_quality.yml +29 -0
  7. data/config/routes.rb +20 -10
  8. data/examples/README_INTEGRATION_TESTING.md +3 -0
  9. data/examples/integration_test_flow.rb +10 -8
  10. data/examples/jobs/federation_cache_refresh_job.rb.example +7 -7
  11. data/examples/jobs/federation_files_generation_job.rb.example +3 -2
  12. data/examples/mock_op_server.rb +4 -5
  13. data/examples/mock_rp_server.rb +3 -3
  14. data/examples/standalone_multiple_endpoints_example.rb +494 -0
  15. data/lib/omniauth_openid_federation/access_token.rb +91 -450
  16. data/lib/omniauth_openid_federation/cache.rb +16 -0
  17. data/lib/omniauth_openid_federation/cache_adapter.rb +6 -3
  18. data/lib/omniauth_openid_federation/constants.rb +3 -0
  19. data/lib/omniauth_openid_federation/entity_statement_reader.rb +39 -14
  20. data/lib/omniauth_openid_federation/federation/entity_statement.rb +0 -4
  21. data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +7 -14
  22. data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +40 -11
  23. data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +8 -91
  24. data/lib/omniauth_openid_federation/federation/signed_jwks.rb +0 -1
  25. data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +54 -21
  26. data/lib/omniauth_openid_federation/federation_endpoint.rb +40 -172
  27. data/lib/omniauth_openid_federation/http_client.rb +145 -32
  28. data/lib/omniauth_openid_federation/http_errors.rb +14 -0
  29. data/lib/omniauth_openid_federation/id_token.rb +19 -0
  30. data/lib/omniauth_openid_federation/jwe.rb +26 -0
  31. data/lib/omniauth_openid_federation/jwks/decode.rb +0 -13
  32. data/lib/omniauth_openid_federation/jwks/fetch.rb +16 -39
  33. data/lib/omniauth_openid_federation/jwks/rotate.rb +45 -20
  34. data/lib/omniauth_openid_federation/jws.rb +3 -11
  35. data/lib/omniauth_openid_federation/jwt_response_decoder.rb +290 -0
  36. data/lib/omniauth_openid_federation/oidc_client.rb +101 -0
  37. data/lib/omniauth_openid_federation/rack.rb +2 -0
  38. data/lib/omniauth_openid_federation/rack_endpoint.rb +21 -9
  39. data/lib/omniauth_openid_federation/rate_limiter.rb +10 -12
  40. data/lib/omniauth_openid_federation/secure_compare.rb +15 -0
  41. data/lib/omniauth_openid_federation/strategy/authorization_request.rb +220 -0
  42. data/lib/omniauth_openid_federation/strategy/callback_handling.rb +135 -0
  43. data/lib/omniauth_openid_federation/strategy/client_construction.rb +101 -0
  44. data/lib/omniauth_openid_federation/strategy/client_entity_statement.rb +211 -0
  45. data/lib/omniauth_openid_federation/strategy/endpoint_resolution.rb +433 -0
  46. data/lib/omniauth_openid_federation/strategy/failure_handling.rb +75 -0
  47. data/lib/omniauth_openid_federation/strategy/id_token_decoding.rb +268 -0
  48. data/lib/omniauth_openid_federation/strategy/jwks_resolution.rb +268 -0
  49. data/lib/omniauth_openid_federation/strategy/provider_entity_statement.rb +134 -0
  50. data/lib/omniauth_openid_federation/strategy/userinfo_decoding.rb +61 -0
  51. data/lib/omniauth_openid_federation/strategy.rb +27 -1910
  52. data/lib/omniauth_openid_federation/tasks/authentication_flow_tester.rb +237 -0
  53. data/lib/omniauth_openid_federation/tasks/callback_processor.rb +235 -0
  54. data/lib/omniauth_openid_federation/tasks/client_keys_preparer.rb +96 -0
  55. data/lib/omniauth_openid_federation/tasks/local_endpoint_tester.rb +189 -0
  56. data/lib/omniauth_openid_federation/tasks/path_resolver.rb +20 -0
  57. data/lib/omniauth_openid_federation/tasks_helper.rb +28 -790
  58. data/lib/omniauth_openid_federation/time_helpers.rb +67 -0
  59. data/lib/omniauth_openid_federation/user_info.rb +15 -0
  60. data/lib/omniauth_openid_federation/utils.rb +14 -9
  61. data/lib/omniauth_openid_federation/validators.rb +55 -44
  62. data/lib/omniauth_openid_federation/version.rb +1 -1
  63. data/lib/omniauth_openid_federation.rb +10 -3
  64. data/lib/tasks/omniauth_openid_federation.rake +5 -5
  65. data/sig/omniauth_openid_federation.rbs +8 -1
  66. data/sig/strategy.rbs +6 -6
  67. metadata +252 -43
@@ -0,0 +1,67 @@
1
+ # Time helper utilities for compatibility with ActiveSupport
2
+ # Provides time methods that work with or without Time.zone
3
+ module OmniauthOpenidFederation
4
+ module TimeHelpers
5
+ # Get current time, using Time.zone if available, otherwise Time
6
+ #
7
+ # @return [Time] Current time
8
+ def self.now
9
+ if time_zone_available?
10
+ Time.zone.now
11
+ else
12
+ # rubocop:disable Rails/TimeZone
13
+ Time.now
14
+ # rubocop:enable Rails/TimeZone
15
+ end
16
+ end
17
+
18
+ # Convert a timestamp to Time, using Time.zone if available, otherwise Time
19
+ #
20
+ # @param timestamp [Integer, Float] Unix timestamp
21
+ # @return [Time] Time object representing the timestamp
22
+ def self.at(timestamp)
23
+ if time_zone_available?
24
+ Time.zone.at(timestamp)
25
+ else
26
+ # rubocop:disable Rails/TimeZone
27
+ Time.at(timestamp)
28
+ # rubocop:enable Rails/TimeZone
29
+ end
30
+ end
31
+
32
+ # Parse a time string, using Time.zone if available, otherwise Time
33
+ #
34
+ # @param time_string [String] Time string to parse
35
+ # @return [Time] Parsed time object
36
+ def self.parse(time_string)
37
+ if time_zone_available?
38
+ Time.zone.parse(time_string)
39
+ else
40
+ # rubocop:disable Rails/TimeZone
41
+ Time.parse(time_string)
42
+ # rubocop:enable Rails/TimeZone
43
+ end
44
+ end
45
+
46
+ # Check if Time.zone is available and configured
47
+ #
48
+ # @return [Boolean] true if Time.zone is available and not nil
49
+ def self.time_zone_available?
50
+ return false unless defined?(ActiveSupport)
51
+ return false unless Time.respond_to?(:zone)
52
+
53
+ begin
54
+ !Time.zone.nil?
55
+ rescue
56
+ false
57
+ end
58
+ end
59
+
60
+ # Seconds to add to {TimeHelpers.now} for JWT request object expiration.
61
+ #
62
+ # @return [Integer]
63
+ def self.request_object_expiration_offset
64
+ Constants::REQUEST_OBJECT_EXPIRATION_SECONDS
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,15 @@
1
+ module OmniauthOpenidFederation
2
+ class UserInfo
3
+ attr_reader :raw_attributes
4
+
5
+ def initialize(raw_attributes)
6
+ @raw_attributes = raw_attributes.each_with_object({}) do |(key, value), claims|
7
+ claims[key.to_sym] = value
8
+ end
9
+ end
10
+
11
+ def as_json(_options = {})
12
+ raw_attributes.transform_keys(&:to_s)
13
+ end
14
+ end
15
+ end
@@ -1,15 +1,25 @@
1
1
  # Utility functions for omniauth_openid_federation
2
+ require_relative "string_helpers"
3
+
2
4
  module OmniauthOpenidFederation
3
5
  module Utils
4
6
  # Convert hash to HashWithIndifferentAccess if available
5
7
  #
6
8
  # @param hash [Hash, Object] The hash to convert
7
9
  # @return [Hash, HashWithIndifferentAccess] Converted hash
8
- def self.to_indifferent_hash(hash)
10
+ def self.to_indifferent_hash(value)
11
+ hash = if value.is_a?(Hash)
12
+ value
13
+ elsif value.respond_to?(:to_h)
14
+ value.to_h
15
+ else
16
+ {}
17
+ end
18
+
9
19
  if defined?(ActiveSupport::HashWithIndifferentAccess)
10
20
  ActiveSupport::HashWithIndifferentAccess.new(hash)
11
21
  else
12
- hash.is_a?(Hash) ? hash : hash.to_h
22
+ hash
13
23
  end
14
24
  end
15
25
 
@@ -18,7 +28,7 @@ module OmniauthOpenidFederation
18
28
  # @param path [String, nil] The file path
19
29
  # @return [String] Sanitized path (filename only)
20
30
  def self.sanitize_path(path)
21
- return "[REDACTED]" if path.nil? || path.empty?
31
+ return "[REDACTED]" if StringHelpers.blank?(path)
22
32
  File.basename(path)
23
33
  end
24
34
 
@@ -27,7 +37,7 @@ module OmniauthOpenidFederation
27
37
  # @param uri [String, nil] The URI
28
38
  # @return [String] Sanitized URI
29
39
  def self.sanitize_uri(uri)
30
- return "[REDACTED]" if uri.nil? || uri.empty?
40
+ return "[REDACTED]" if StringHelpers.blank?(uri)
31
41
  begin
32
42
  parsed = URI.parse(uri)
33
43
  "#{parsed.scheme}://#{parsed.host}/[REDACTED]"
@@ -70,16 +80,13 @@ module OmniauthOpenidFederation
70
80
  def self.validate_file_path!(path, allowed_dirs: nil)
71
81
  raise SecurityError, "File path cannot be nil" if path.nil?
72
82
 
73
- # Convert Pathname to string if needed
74
83
  path_str = path.to_s
75
84
  raise SecurityError, "File path cannot be empty" if path_str.empty?
76
85
 
77
- # Check for path traversal attempts
78
86
  if path_str.include?("..") || path_str.include?("~")
79
87
  raise SecurityError, "Path traversal detected in: #{sanitize_path(path_str)}"
80
88
  end
81
89
 
82
- # Resolve to absolute path
83
90
  resolved = File.expand_path(path_str)
84
91
 
85
92
  # Validate it's within allowed directories if specified
@@ -120,7 +127,6 @@ module OmniauthOpenidFederation
120
127
  n = Base64.urlsafe_encode64(key.n.to_s(2), padding: false)
121
128
  e = Base64.urlsafe_encode64(key.e.to_s(2), padding: false)
122
129
 
123
- # Generate kid (key ID) from public key
124
130
  public_key_pem = key.public_key.to_pem
125
131
  kid = Digest::SHA256.hexdigest(public_key_pem)[0, 16]
126
132
 
@@ -131,7 +137,6 @@ module OmniauthOpenidFederation
131
137
  e: e
132
138
  }
133
139
 
134
- # Add use field if specified
135
140
  jwk[:use] = use if use
136
141
 
137
142
  jwk
@@ -1,6 +1,7 @@
1
1
  # Input validation utilities for omniauth_openid_federation
2
2
  require_relative "constants"
3
3
  require_relative "configuration"
4
+ require_relative "string_helpers"
4
5
 
5
6
  module OmniauthOpenidFederation
6
7
  module Validators
@@ -8,20 +9,54 @@ module OmniauthOpenidFederation
8
9
  #
9
10
  # @param private_key [OpenSSL::PKey::RSA, String, nil] The private key to validate
10
11
  # @raise [ConfigurationError] If private key is missing or invalid
11
- def self.validate_private_key!(private_key)
12
+ def self.validate_private_key!(private_key, min_bits: Constants::MIN_RSA_KEY_BITS)
12
13
  if private_key.nil?
13
14
  raise ConfigurationError, "Private key is required for signed request objects"
14
15
  end
15
16
 
16
- # Try to parse if it's a string
17
- if private_key.is_a?(String)
18
- begin
19
- OpenSSL::PKey::RSA.new(private_key)
20
- rescue => e
21
- raise ConfigurationError, "Invalid private key format: #{e.message}"
17
+ normalized_key =
18
+ if private_key.is_a?(String)
19
+ begin
20
+ OpenSSL::PKey::RSA.new(private_key)
21
+ rescue => error
22
+ raise ConfigurationError, "Invalid private key format: #{error.message}"
23
+ end
24
+ elsif private_key.is_a?(OpenSSL::PKey::RSA)
25
+ private_key
26
+ else
27
+ raise ConfigurationError, "Private key must be an OpenSSL::PKey::RSA instance or PEM string"
22
28
  end
23
- elsif !private_key.is_a?(OpenSSL::PKey::RSA)
24
- raise ConfigurationError, "Private key must be an OpenSSL::PKey::RSA instance or PEM string"
29
+
30
+ if min_bits.to_i.positive? && normalized_key.n.num_bits < min_bits.to_i
31
+ raise ConfigurationError,
32
+ "RSA private key must be at least #{min_bits} bits (got #{normalized_key.n.num_bits} bits)"
33
+ end
34
+
35
+ true
36
+ end
37
+
38
+ def self.validate_required_request_object_claims!(claims, required_names)
39
+ required = Array(required_names).map(&:to_s).uniq
40
+ return true if required.empty?
41
+
42
+ normalized_claims = (claims || {}).each_with_object({}) do |(key, value), hash|
43
+ hash[key.to_s] = value
44
+ end
45
+
46
+ missing = required.reject { |name| StringHelpers.present?(normalized_claims[name]) }
47
+ return true if missing.empty?
48
+
49
+ raise ConfigurationError, "Missing required request object claims: #{missing.join(", ")}"
50
+ end
51
+
52
+ def self.validate_allowed_acr_value!(acr_value, allowed_acr_values)
53
+ allowed = Array(allowed_acr_values).map(&:to_s).map(&:strip).reject { |value| StringHelpers.blank?(value) }
54
+ return true if allowed.empty?
55
+
56
+ token_acr = acr_value.to_s.strip
57
+ if StringHelpers.blank?(token_acr) || !allowed.include?(token_acr)
58
+ raise ValidationError,
59
+ "ID token acr mismatch: expected one of [#{allowed.join(", ")}], got '#{acr_value}'"
25
60
  end
26
61
 
27
62
  true
@@ -82,10 +117,8 @@ module OmniauthOpenidFederation
82
117
  def self.validate_client_options!(client_options)
83
118
  client_options ||= {}
84
119
 
85
- # Normalize hash keys to symbols
86
120
  normalized = normalize_hash(client_options)
87
121
 
88
- # Validate required fields (structure validation, not security)
89
122
  if StringHelpers.blank?(normalized[:identifier])
90
123
  raise ConfigurationError, "Client identifier is required"
91
124
  end
@@ -94,8 +127,6 @@ module OmniauthOpenidFederation
94
127
  raise ConfigurationError, "Redirect URI is required"
95
128
  end
96
129
 
97
- # Basic format check for redirect URI (config validation, not security)
98
- # Note: Config values are trusted, we only check format to catch config errors
99
130
  begin
100
131
  parsed = URI.parse(normalized[:redirect_uri].to_s)
101
132
  unless parsed.is_a?(URI::HTTP) || parsed.is_a?(URI::HTTPS)
@@ -105,10 +136,8 @@ module OmniauthOpenidFederation
105
136
  raise ConfigurationError, "Invalid redirect URI format: #{e.message}"
106
137
  end
107
138
 
108
- # Validate private key (required for security)
109
139
  validate_private_key!(normalized[:private_key])
110
140
 
111
- # Basic format check for endpoints (config validation, not security)
112
141
  %i[authorization_endpoint token_endpoint jwks_uri].each do |endpoint|
113
142
  if normalized.key?(endpoint) && !StringHelpers.blank?(normalized[endpoint])
114
143
  # Endpoints can be paths or full URLs
@@ -145,7 +174,6 @@ module OmniauthOpenidFederation
145
174
  str = value.to_s.strip
146
175
  return nil if str.length > max_length
147
176
 
148
- # Only allow printable ASCII (whitelist approach)
149
177
  unless allow_control_chars
150
178
  str = str.gsub(/[^\x20-\x7E]/, "")
151
179
  end
@@ -159,15 +187,16 @@ module OmniauthOpenidFederation
159
187
  max_length ||= ::OmniauthOpenidFederation::Configuration.config.max_string_length
160
188
  raise SecurityError, "URI cannot be nil" if uri_str.nil?
161
189
 
162
- str = uri_str.to_s.strip
190
+ original_str = uri_str.to_s
191
+ sanitized = original_str.gsub(/[^\x20-\x7E]/, "")
192
+ raise SecurityError, "URI contains invalid characters (only printable ASCII allowed)" if sanitized != original_str
193
+
194
+ str = sanitized.strip
163
195
  raise SecurityError, "URI cannot be empty" if str.empty?
164
196
  raise SecurityError, "URI exceeds maximum length of #{max_length} characters" if str.length > max_length
165
197
 
166
- sanitized = str.gsub(/[^\x20-\x7E]/, "")
167
- raise SecurityError, "URI contains invalid characters (only printable ASCII allowed)" if sanitized != str
168
-
169
198
  begin
170
- parsed = URI.parse(sanitized)
199
+ parsed = URI.parse(str)
171
200
  rescue URI::InvalidURIError => e
172
201
  raise SecurityError, "Invalid URI format: #{e.message}"
173
202
  end
@@ -180,7 +209,7 @@ module OmniauthOpenidFederation
180
209
  raise SecurityError, "URI must be HTTP or HTTPS"
181
210
  end
182
211
 
183
- raise SecurityError, "URI host cannot be empty" if parsed.host.nil? || parsed.host.empty?
212
+ raise SecurityError, "URI host cannot be empty" if StringHelpers.blank?(parsed.host)
184
213
 
185
214
  parsed
186
215
  end
@@ -199,22 +228,16 @@ module OmniauthOpenidFederation
199
228
 
200
229
  case acr_values
201
230
  when Array
202
- # Filter out blanks (arrays may already be sanitized)
203
231
  values = acr_values.map(&:to_s).map(&:strip).reject { |v| StringHelpers.blank?(v) }
204
232
  when String
205
- # Trim and split by whitespace and validate each value using allowed characters
206
- # Security: Use simple space split (no regexp to avoid ReDoS)
207
233
  trimmed = acr_values.strip
208
234
  values = trimmed.split(" ").map(&:strip).reject { |v| StringHelpers.blank?(v) }
209
235
  else
210
- # Convert to string, trim and split
211
236
  str = acr_values.to_s.strip
212
237
  return nil if str.length > max_length
213
- # Security: Use simple space split (no regexp to avoid ReDoS)
214
238
  values = str.split(" ").map(&:strip).reject { |v| StringHelpers.blank?(v) }
215
239
  end
216
240
 
217
- # Sanitize each value unless already sanitized
218
241
  unless skip_sanitization
219
242
  values = values.map { |v| sanitize_request_param(v) }.compact
220
243
  end
@@ -243,9 +266,8 @@ module OmniauthOpenidFederation
243
266
  raise ConfigurationError, "client_id cannot be empty after trimming"
244
267
  end
245
268
 
246
- # Sanitize using allowed characters (printable ASCII only)
247
269
  sanitized = sanitize_request_param(str)
248
- if sanitized.nil? || sanitized.empty?
270
+ if StringHelpers.blank?(sanitized)
249
271
  raise ConfigurationError, "client_id contains invalid characters"
250
272
  end
251
273
 
@@ -268,7 +290,6 @@ module OmniauthOpenidFederation
268
290
  raise ConfigurationError, "redirect_uri cannot be empty after trimming"
269
291
  end
270
292
 
271
- # Validate as URI (includes allowed characters validation)
272
293
  validated = validate_uri_safe!(str, allowed_schemes: ["http", "https"])
273
294
  validated.to_s
274
295
  end
@@ -289,7 +310,6 @@ module OmniauthOpenidFederation
289
310
  return nil
290
311
  end
291
312
 
292
- # Normalize to array
293
313
  scopes = case scope
294
314
  when Array
295
315
  scope.map(&:to_s).map(&:strip).reject { |s| StringHelpers.blank?(s) }
@@ -299,14 +319,12 @@ module OmniauthOpenidFederation
299
319
  scope.to_s.strip.split(" ").map(&:strip).reject { |s| StringHelpers.blank?(s) }
300
320
  end
301
321
 
302
- # Validate each scope value (allowed: printable ASCII)
303
322
  scopes = scopes.map { |s| sanitize_request_param(s) }.compact
304
323
 
305
324
  if scopes.empty?
306
325
  raise ConfigurationError, "scope cannot be empty after validation"
307
326
  end
308
327
 
309
- # Check for "openid" scope if required
310
328
  if require_openid && !scopes.include?("openid")
311
329
  raise ConfigurationError, "scope MUST include 'openid' per OIDC Core 1.0 spec"
312
330
  end
@@ -336,9 +354,8 @@ module OmniauthOpenidFederation
336
354
  raise ConfigurationError, "state cannot be empty after trimming"
337
355
  end
338
356
 
339
- # Sanitize using allowed characters (printable ASCII only)
340
357
  sanitized = sanitize_request_param(str)
341
- if sanitized.nil? || sanitized.empty?
358
+ if StringHelpers.blank?(sanitized)
342
359
  raise ConfigurationError, "state contains invalid characters"
343
360
  end
344
361
 
@@ -363,10 +380,8 @@ module OmniauthOpenidFederation
363
380
  return nil
364
381
  end
365
382
 
366
- # Sanitize using allowed characters (printable ASCII only)
367
- # OIDC Core 1.0: nonce value is a case-sensitive string
368
383
  sanitized = sanitize_request_param(str)
369
- if sanitized.nil? || sanitized.empty?
384
+ if StringHelpers.blank?(sanitized)
370
385
  if required
371
386
  raise ConfigurationError, "nonce contains invalid characters"
372
387
  end
@@ -392,13 +407,11 @@ module OmniauthOpenidFederation
392
407
  raise ConfigurationError, "response_type cannot be empty after trimming"
393
408
  end
394
409
 
395
- # Sanitize using allowed characters (printable ASCII only)
396
410
  sanitized = sanitize_request_param(str)
397
- if sanitized.nil? || sanitized.empty?
411
+ if StringHelpers.blank?(sanitized)
398
412
  raise ConfigurationError, "response_type contains invalid characters"
399
413
  end
400
414
 
401
- # Validate it's a known response type (space-separated list)
402
415
  valid_types = ["code", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token"]
403
416
  types = sanitized.split(" ").map(&:strip)
404
417
  unless types.all? { |t| valid_types.include?(t) || t.match?(/^[a-z_]+$/) }
@@ -426,10 +439,8 @@ module OmniauthOpenidFederation
426
439
  raise SecurityError, "Entity identifier cannot be empty after trimming"
427
440
  end
428
441
 
429
- # Validate as URI (includes allowed characters and length validation)
430
442
  validate_uri_safe!(str, max_length: max_length, allowed_schemes: ["http", "https"])
431
443
 
432
- # Return trimmed and validated value
433
444
  str
434
445
  end
435
446
  end
@@ -1,3 +1,3 @@
1
1
  module OmniauthOpenidFederation
2
- VERSION = "1.3.0".freeze
2
+ VERSION = "2.0.0".freeze
3
3
  end
@@ -1,7 +1,7 @@
1
1
  # OmniAuth OpenID Federation
2
2
  #
3
- # Custom OmniAuth strategy for OpenID Federation providers using openid_connect gem
4
- # supporting signed request objects, ID token encryption, and OpenID Federation.
3
+ # Custom OmniAuth strategy for OpenID Federation providers supporting signed request objects,
4
+ # ID token encryption, and OpenID Federation.
5
5
  #
6
6
  # @see https://openid.net/specs/openid-federation-1_0.html
7
7
  module OmniauthOpenidFederation
@@ -28,6 +28,7 @@ end
28
28
 
29
29
  require_relative "omniauth_openid_federation/version"
30
30
  require_relative "omniauth_openid_federation/string_helpers"
31
+ require_relative "omniauth_openid_federation/time_helpers"
31
32
  require_relative "omniauth_openid_federation/logger"
32
33
  require_relative "omniauth_openid_federation/configuration"
33
34
  require_relative "omniauth_openid_federation/errors"
@@ -38,7 +39,13 @@ require_relative "omniauth_openid_federation/constants"
38
39
  require_relative "omniauth_openid_federation/cache"
39
40
  require_relative "omniauth_openid_federation/cache_adapter"
40
41
  require_relative "omniauth_openid_federation/utils"
42
+ require_relative "omniauth_openid_federation/jwe"
41
43
  require_relative "omniauth_openid_federation/jws"
44
+ require_relative "omniauth_openid_federation/http_errors"
45
+ require_relative "omniauth_openid_federation/id_token"
46
+ require_relative "omniauth_openid_federation/user_info"
47
+ require_relative "omniauth_openid_federation/secure_compare"
48
+ require_relative "omniauth_openid_federation/oidc_client"
42
49
  require_relative "omniauth_openid_federation/jwks/normalizer"
43
50
  require_relative "omniauth_openid_federation/jwks/fetch"
44
51
  require_relative "omniauth_openid_federation/jwks/decode"
@@ -55,12 +62,12 @@ require_relative "omniauth_openid_federation/federation/trust_chain_resolver"
55
62
  require_relative "omniauth_openid_federation/federation/metadata_policy_merger"
56
63
  require_relative "omniauth_openid_federation/federation/signed_jwks"
57
64
  require_relative "omniauth_openid_federation/federation_endpoint"
58
- require_relative "omniauth_openid_federation/rack_endpoint"
59
65
  require_relative "omniauth_openid_federation/entity_statement_reader"
60
66
  require_relative "omniauth_openid_federation/tasks_helper"
61
67
  require_relative "omniauth_openid_federation/endpoint_resolver"
62
68
  require_relative "omniauth_openid_federation/strategy"
63
69
  require_relative "omniauth_openid_federation/access_token"
70
+ require_relative "omniauth_openid_federation/jwt_response_decoder"
64
71
 
65
72
  module OmniauthOpenidFederation
66
73
  # Rotate JWKS cache for a provider
@@ -1,5 +1,6 @@
1
1
  # Rake tasks for OmniAuth OpenID Federation
2
2
  # Thin wrappers around TasksHelper for CLI interface
3
+ require_relative "../omniauth_openid_federation/time_helpers"
3
4
 
4
5
  namespace :openid_federation do
5
6
  desc "Fetch entity statement from OpenID Federation provider"
@@ -262,8 +263,8 @@ namespace :openid_federation do
262
263
  end
263
264
  puts " Issuer: #{metadata[:issuer]}"
264
265
  puts " Subject: #{metadata[:sub]}"
265
- puts " Expires: #{Time.at(metadata[:exp])}" if metadata[:exp]
266
- puts " Issued At: #{Time.at(metadata[:iat])}" if metadata[:iat]
266
+ puts " Expires: #{OmniauthOpenidFederation::TimeHelpers.at(metadata[:exp])}" if metadata[:exp]
267
+ puts " Issued At: #{OmniauthOpenidFederation::TimeHelpers.at(metadata[:iat])}" if metadata[:iat]
267
268
  puts
268
269
 
269
270
  # Key status information
@@ -456,8 +457,7 @@ namespace :openid_federation do
456
457
  begin
457
458
  well_known_url = "#{base_url}/.well-known/openid-federation"
458
459
  puts "📥 Attempting to fetch entity statement from: #{well_known_url}"
459
- require "http"
460
- response = HTTP.timeout(connect: 5, read: 5).get(well_known_url)
460
+ response = OmniauthOpenidFederation::HttpClient.get(well_known_url, timeout: 5, max_retries: 0)
461
461
  if response.status.success?
462
462
  entity_statement_path = "/tmp/entity_statement_#{Time.now.to_i}.json"
463
463
  File.write(entity_statement_path, response.body.to_s)
@@ -594,7 +594,7 @@ namespace :openid_federation do
594
594
  if value
595
595
  if [:exp, :iat, :auth_time].include?(key)
596
596
  time_value = begin
597
- Time.at(value)
597
+ OmniauthOpenidFederation::TimeHelpers.at(value)
598
598
  rescue
599
599
  value
600
600
  end
@@ -71,7 +71,8 @@ module OmniauthOpenidFederation
71
71
  end
72
72
 
73
73
  class HttpClient
74
- def self.get: (String uri, ?timeout: Integer, ?max_retries: Integer, ?retry_delay: Integer) -> untyped
74
+ def self.get: (String uri, ?Hash[Symbol, untyped] options) -> untyped
75
+ def self.post: (String uri, ?Hash[Symbol, untyped] options) -> untyped
75
76
  end
76
77
 
77
78
  class Logger
@@ -183,6 +184,12 @@ module OmniauthOpenidFederation
183
184
  def self.blank?: (untyped value) -> bool
184
185
  end
185
186
 
187
+ module TimeHelpers
188
+ def self.now: () -> Time
189
+ def self.at: (Integer | Float timestamp) -> Time
190
+ def self.parse: (String time_string) -> Time
191
+ end
192
+
186
193
  module Validators
187
194
  def self.validate_private_key!: (untyped private_key) -> void
188
195
  def self.normalize_hash: (untyped hash) -> Hash[Symbol, untyped]
data/sig/strategy.rbs CHANGED
@@ -1,8 +1,8 @@
1
1
  module OmniAuth
2
2
  module Strategies
3
3
  class OpenIDFederation < OmniAuth::Strategies::OAuth2
4
- def client: () -> untyped
5
- def oidc_client: () -> untyped
4
+ def client: () -> OmniauthOpenidFederation::OidcClient
5
+ def oidc_client: () -> OmniauthOpenidFederation::OidcClient
6
6
  def request_phase: () -> untyped
7
7
  def callback_phase: () -> untyped
8
8
  def auth_hash: () -> OmniAuth::AuthHash
@@ -10,7 +10,7 @@ module OmniAuth
10
10
  def raw_info: () -> Hash[String, untyped]
11
11
  def client_jwk_signing_key: () -> untyped
12
12
  def options: () -> untyped
13
- def exchange_authorization_code: (String authorization_code) -> untyped
13
+ def exchange_authorization_code: (String authorization_code) -> OmniauthOpenidFederation::AccessToken
14
14
  def new_state: () -> String
15
15
  def new_nonce: () -> String
16
16
  def resolve_endpoints_from_metadata: (Hash[Symbol, untyped] client_options_hash) -> Hash[Symbol, String?]
@@ -21,7 +21,7 @@ module OmniAuth
21
21
  def resolve_jwks_uri: (Hash[Symbol, untyped] normalized_options) -> String?
22
22
  def build_base_url: (Hash[Symbol, untyped] client_options_hash) -> String
23
23
  def build_endpoint: (String base_url, String path) -> String
24
- def decode_id_token: (String id_token) -> Hash[String, untyped]
24
+ def decode_id_token: (String id_token) -> OmniauthOpenidFederation::IdToken
25
25
  def encrypted_token?: (String token) -> bool
26
26
  def decode_userinfo: (untyped userinfo) -> Hash[String, untyped]
27
27
  def load_provider_entity_statement: () -> Hash[Symbol, untyped]?
@@ -45,9 +45,10 @@ module OmniAuth
45
45
  end
46
46
  end
47
47
 
48
- module OpenIDConnect
48
+ module OmniauthOpenidFederation
49
49
  class AccessToken
50
50
  def resource_request: () { () -> untyped } -> untyped
51
+ def userinfo!: () -> untyped
51
52
  def get_strategy_options: () -> Hash[Symbol, untyped]
52
53
  def extract_encryption_key_for_decryption: () -> untyped
53
54
  def fetch_signed_jwks: (Hash[Symbol, untyped] strategy_options) -> Hash[String, untyped]?
@@ -55,4 +56,3 @@ module OpenIDConnect
55
56
  def resolve_jwks_uri_from_entity_statement: (Hash[Symbol, untyped] strategy_options) -> String?
56
57
  end
57
58
  end
58
-