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
@@ -1,8 +1,19 @@
1
+ require "http"
2
+ require "openssl"
1
3
  require_relative "constants"
2
4
 
3
5
  # HTTP client with retry logic and SSL configuration
4
6
  module OmniauthOpenidFederation
5
7
  class HttpClient
8
+ RETRYABLE_ERRORS = [
9
+ HTTP::Error,
10
+ Timeout::Error,
11
+ Errno::ECONNREFUSED,
12
+ Errno::ETIMEDOUT
13
+ ].freeze
14
+
15
+ RETRYABLE_HTTP_STATUS_CODES = [429, 502, 503].freeze
16
+
6
17
  # Execute an HTTP GET request with retry logic
7
18
  #
8
19
  # @param uri [String, URI] The URI to fetch
@@ -10,61 +21,163 @@ module OmniauthOpenidFederation
10
21
  # @option options [Integer] :max_retries Maximum number of retries (default: from config)
11
22
  # @option options [Integer] :retry_delay Base retry delay in seconds (default: from config)
12
23
  # @option options [Integer] :timeout Request timeout in seconds (default: from config)
24
+ # @option options [Integer] :connect_timeout Connect timeout in seconds
25
+ # @option options [Integer] :read_timeout Read timeout in seconds
26
+ # @option options [Hash] :headers Request headers
13
27
  # @return [HTTP::Response] The HTTP response
14
28
  # @raise [NetworkError] If the request fails after all retries
15
29
  def self.get(uri, options = {})
16
- max_retries = options[:max_retries] || Configuration.config.max_retries
17
- retry_delay = options[:retry_delay] || Configuration.config.retry_delay
18
- timeout = options[:timeout] || Configuration.config.http_timeout
30
+ request(:get, uri, options)
31
+ end
32
+
33
+ # Execute an HTTP POST request with retry logic
34
+ #
35
+ # POST defaults to max_retries: 0 because retries are not safe for non-idempotent requests.
36
+ # Pass max_retries explicitly when duplicate POST attempts are acceptable.
37
+ #
38
+ # @param uri [String, URI] The URI to post to
39
+ # @param options [Hash] Request options (same as get, plus :form)
40
+ # @option options [Hash] :form Form parameters
41
+ # @return [HTTP::Response] The HTTP response
42
+ # @raise [NetworkError] If the request fails after all retries
43
+ def self.post(uri, options = {})
44
+ request(:post, uri, options)
45
+ end
19
46
 
20
- http_client = build_http_client(timeout)
47
+ def self.request(method, uri, options = {})
48
+ max_retries = max_retries_for(options, method)
49
+ retry_delay = options[:retry_delay] || Configuration.config.retry_delay
50
+ http_client = build_http_client(resolve_timeout(options))
51
+ headers = options[:headers] || {}
52
+ form = options[:form]
53
+ max_attempts = max_retries + 1
21
54
 
22
55
  retries = 0
23
- begin
24
- http_client.get(uri)
25
- rescue HTTP::Error, Timeout::Error, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
26
- retries += 1
27
- if retries > max_retries
28
- error_msg = "Failed to fetch #{uri} after #{max_retries} retries: #{e.class} - #{e.message}"
29
- OmniauthOpenidFederation::Logger.error("[HttpClient] #{error_msg}")
30
- raise OmniauthOpenidFederation::NetworkError, error_msg, e.backtrace
56
+
57
+ loop do
58
+ response = perform_request(http_client, headers, method, uri, form)
59
+
60
+ if should_retry_for_status?(method, response, retries, max_retries)
61
+ retries = retry_after_status(response, retries, max_attempts, retry_delay)
62
+ next
31
63
  end
32
64
 
33
- delay = [retry_delay * retries, Constants::MAX_RETRY_DELAY_SECONDS].min
34
- OmniauthOpenidFederation::Logger.warn("[HttpClient] Request failed (attempt #{retries}/#{max_retries}), retrying in #{delay}s: #{e.message}")
35
- sleep(delay)
36
- retry
65
+ return response
66
+ rescue *RETRYABLE_ERRORS => error
67
+ retries = handle_network_error(method, uri, error, retries, max_retries, max_attempts, retry_delay)
37
68
  end
38
69
  end
39
70
 
40
- # Build HTTP client with SSL configuration
41
- #
42
- # @param timeout [Integer] Request timeout in seconds
43
- # @return [HTTP::Client] Configured HTTP client
44
- def self.build_http_client(timeout)
45
- http_options_hash = build_http_options_hash || {}
46
- http_options = HTTP::Options.new(http_options_hash)
47
- HTTP::Client.new(http_options).timeout(timeout)
71
+ def self.perform_request(http_client, headers, method, uri, form)
72
+ client = http_client
73
+ client = client.headers(headers) unless headers.empty?
74
+
75
+ case method
76
+ when :get
77
+ client.get(uri)
78
+ when :post
79
+ client.post(uri, form: form || {})
80
+ else
81
+ raise ArgumentError, "Unsupported HTTP method: #{method}"
82
+ end
83
+ end
84
+
85
+ def self.should_retry_for_status?(method, response, retries, max_retries)
86
+ method == :get && retryable_http_status?(response.status.code) && retries < max_retries
87
+ end
88
+
89
+ def self.retry_after_status(response, retries, max_attempts, retry_delay)
90
+ retries += 1
91
+ delay = retry_delay_for(retries, retry_delay)
92
+ OmniauthOpenidFederation::Logger.warn(
93
+ "[HttpClient] HTTP #{response.status.code} on attempt #{retries}/#{max_attempts}, retrying in #{delay}s"
94
+ )
95
+ sleep(delay)
96
+ retries
97
+ end
98
+
99
+ def self.handle_network_error(method, uri, error, retries, max_retries, max_attempts, retry_delay)
100
+ retries += 1
101
+ if retries > max_retries
102
+ error_message = "Failed to #{method.to_s.upcase} #{uri} after #{max_attempts} attempts: #{error.class} - #{error.message}"
103
+ OmniauthOpenidFederation::Logger.error("[HttpClient] #{error_message}")
104
+ raise OmniauthOpenidFederation::NetworkError, error_message, error.backtrace
105
+ end
106
+
107
+ delay = retry_delay_for(retries, retry_delay)
108
+ OmniauthOpenidFederation::Logger.warn(
109
+ "[HttpClient] Request failed on attempt #{retries}/#{max_attempts}, retrying in #{delay}s: #{error.message}"
110
+ )
111
+ sleep(delay)
112
+ retries
113
+ end
114
+
115
+ def self.build_http_client(timeout_config)
116
+ http_options = HTTP::Options.new(build_http_options_hash)
117
+ HTTP::Client.new(http_options).timeout(timeout_config)
48
118
  end
49
119
 
50
- # Build HTTP options hash from configuration
51
- #
52
- # @return [Hash, nil] HTTP options hash or nil
53
120
  def self.build_http_options_hash
54
121
  config = Configuration.config
55
-
56
- # If http_options is configured, use it (can be Hash or Proc)
57
- if config.http_options
122
+ user_options = if config.http_options
58
123
  if config.http_options.is_a?(Proc)
59
124
  config.http_options.call
60
125
  else
61
126
  config.http_options
62
127
  end
63
128
  end
129
+
130
+ options = (user_options || {}).dup
131
+ options[:ssl] = (options[:ssl] || {}).dup
132
+ options[:ssl][:verify_mode] ||= ssl_verify_mode(config.verify_ssl)
133
+ if options[:ssl][:verify_mode] == OpenSSL::SSL::VERIFY_PEER && !options[:ssl][:ca_file]
134
+ ca_file = ssl_ca_file
135
+ options[:ssl][:ca_file] = ca_file if ca_file
136
+ end
137
+ options
138
+ end
139
+
140
+ def self.ssl_verify_mode(verify_ssl)
141
+ verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
142
+ end
143
+
144
+ def self.ssl_ca_file
145
+ if ENV["SSL_CERT_FILE"] && File.file?(ENV["SSL_CERT_FILE"])
146
+ ENV["SSL_CERT_FILE"]
147
+ elsif File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE)
148
+ OpenSSL::X509::DEFAULT_CERT_FILE
149
+ end
150
+ end
151
+
152
+ def self.max_retries_for(options, method)
153
+ return options[:max_retries] if options.key?(:max_retries)
154
+
155
+ (method == :post) ? 0 : Configuration.config.max_retries
64
156
  end
65
157
 
66
- private_class_method :build_http_options_hash
158
+ def self.resolve_timeout(options)
159
+ default_timeout = Configuration.config.http_timeout
160
+
161
+ if options[:connect_timeout] || options[:read_timeout]
162
+ {
163
+ connect: options[:connect_timeout] || default_timeout,
164
+ read: options[:read_timeout] || default_timeout
165
+ }
166
+ else
167
+ options[:timeout] || default_timeout
168
+ end
169
+ end
170
+
171
+ def self.retryable_http_status?(status_code)
172
+ RETRYABLE_HTTP_STATUS_CODES.include?(status_code)
173
+ end
174
+
175
+ def self.retry_delay_for(retry_count, base_delay)
176
+ [base_delay * retry_count, Constants::MAX_RETRY_DELAY_SECONDS].min
177
+ end
67
178
 
68
- private_class_method :build_http_client
179
+ private_class_method :request, :perform_request, :should_retry_for_status?, :retry_after_status,
180
+ :handle_network_error, :build_http_client, :build_http_options_hash, :ssl_verify_mode, :ssl_ca_file,
181
+ :max_retries_for, :resolve_timeout, :retryable_http_status?, :retry_delay_for
69
182
  end
70
183
  end
@@ -0,0 +1,14 @@
1
+ module OmniauthOpenidFederation
2
+ class HttpError < Error
3
+ attr_reader :response
4
+
5
+ def initialize(status, message = nil, response = nil)
6
+ @response = response
7
+ super(message || "HTTP error #{status}")
8
+ end
9
+ end
10
+
11
+ class BadRequest < HttpError; end
12
+ class Unauthorized < HttpError; end
13
+ class Forbidden < HttpError; end
14
+ end
@@ -0,0 +1,19 @@
1
+ module OmniauthOpenidFederation
2
+ class IdToken
3
+ CLAIM_READERS = %i[iss sub aud exp iat nonce acr auth_time amr].freeze
4
+
5
+ attr_reader :raw_attributes
6
+
7
+ def initialize(raw_attributes)
8
+ @raw_attributes = raw_attributes.each_with_object({}) do |(key, value), claims|
9
+ claims[key.to_sym] = value
10
+ end
11
+ end
12
+
13
+ CLAIM_READERS.each do |claim_name|
14
+ define_method(claim_name) do
15
+ raw_attributes[claim_name]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ require "jwe"
2
+
3
+ require_relative "errors"
4
+
5
+ module OmniauthOpenidFederation
6
+ # Compact-serialization JWE helpers for nested JWTs (sign then encrypt).
7
+ module Jwe
8
+ PARTS_COUNT = 5
9
+
10
+ def self.encrypted?(token)
11
+ token.to_s.count(".") + 1 == PARTS_COUNT
12
+ end
13
+
14
+ def self.encrypt(plaintext, public_key, alg:, enc:)
15
+ ::JWE.encrypt(plaintext.to_s, public_key, alg: alg.to_s, enc: enc.to_s)
16
+ end
17
+
18
+ def self.decrypt(ciphertext, private_key)
19
+ ::JWE.decrypt(ciphertext.to_s, private_key)
20
+ rescue ::JWE::DecodeError, ::JWE::InvalidData, ::JWE::BadCEK, ::JWE::NotImplementedError, ArgumentError => error
21
+ raise OmniauthOpenidFederation::DecryptionError,
22
+ "Failed to decrypt JWE: #{error.message}",
23
+ error.backtrace
24
+ end
25
+ end
26
+ end
@@ -113,19 +113,6 @@ module OmniauthOpenidFederation
113
113
  else
114
114
  raise e
115
115
  end
116
- rescue => e
117
- # Other errors might be due to key rotation
118
- if retried
119
- # If already re-tried to fetch, raise error
120
- error_msg = "JWT decode failed after cache refresh: #{e.class} - #{e.message}"
121
- OmniauthOpenidFederation::Logger.error("[Jwks::Decode] #{error_msg}")
122
- raise ValidationError, error_msg, e.backtrace
123
- else
124
- OmniauthOpenidFederation::Logger.warn("[Jwks::Decode] JWT decode error (clearing cache and retrying - possible key rotation): #{e.class} - #{e.message}")
125
- # Reset cache to force re-fetching of keys
126
- OmniauthOpenidFederation::Cache.delete_jwks(jwks_uri)
127
- run(encoded_jwt, jwks_uri, retried: true, entity_statement_keys: entity_statement_keys, &block)
128
- end
129
116
  end
130
117
  end
131
118
 
@@ -1,4 +1,3 @@
1
- require "http"
2
1
  require "jwt"
3
2
  require "openssl"
4
3
  require_relative "../logger"
@@ -44,52 +43,30 @@ module OmniauthOpenidFederation
44
43
  cache_ttl ||= config.cache_ttl
45
44
  rotate_on_errors = config.rotate_on_errors
46
45
 
47
- # Use cache adapter if available, otherwise fetch directly
48
46
  if CacheAdapter.available?
49
- if force_refresh
50
- # Force refresh: clear cache and fetch fresh
51
- CacheAdapter.delete(cache_key)
52
- end
47
+ CacheAdapter.delete(cache_key) if force_refresh
53
48
 
54
- if cache_ttl.nil?
55
- # Manual rotation: cache forever, only rotate on errors if rotate_on_errors is enabled
56
- begin
57
- CacheAdapter.fetch(cache_key, expires_in: nil) do
58
- fetch_jwks(jwks_uri, entity_statement_keys)
59
- end
60
- rescue KeyRelatedError => e
61
- # Rotate on key-related errors if configured
62
- if rotate_on_errors
63
- OmniauthOpenidFederation::Logger.warn("[Jwks::Fetch] Key-related error detected, rotating cache: #{e.message}")
64
- CacheAdapter.delete(cache_key)
65
- fetch_jwks(jwks_uri, entity_statement_keys)
66
- else
67
- raise
68
- end
69
- end
70
- else
71
- # TTL-based cache: expires after cache_ttl seconds
72
- # Rotate on errors if configured
73
- begin
74
- CacheAdapter.fetch(cache_key, expires_in: cache_ttl) do
75
- fetch_jwks(jwks_uri, entity_statement_keys)
76
- end
77
- rescue KeyRelatedError => e
78
- # Rotate on key-related errors if configured
79
- if rotate_on_errors
80
- OmniauthOpenidFederation::Logger.warn("[Jwks::Fetch] Key-related error detected, rotating cache: #{e.message}")
81
- CacheAdapter.delete(cache_key)
82
- fetch_jwks(jwks_uri, entity_statement_keys)
83
- else
84
- raise
85
- end
86
- end
49
+ fetch_from_cache(cache_key, cache_ttl: cache_ttl, rotate_on_errors: rotate_on_errors) do
50
+ fetch_jwks(jwks_uri, entity_statement_keys)
87
51
  end
88
52
  else
89
53
  fetch_jwks(jwks_uri, entity_statement_keys)
90
54
  end
91
55
  end
92
56
 
57
+ def self.fetch_from_cache(cache_key, cache_ttl:, rotate_on_errors:)
58
+ CacheAdapter.fetch(cache_key, expires_in: cache_ttl) { yield }
59
+ rescue KeyRelatedError => error
60
+ if rotate_on_errors
61
+ OmniauthOpenidFederation::Logger.warn("[Jwks::Fetch] Key-related error detected, rotating cache: #{error.message}")
62
+ CacheAdapter.delete(cache_key)
63
+ yield
64
+ else
65
+ raise
66
+ end
67
+ end
68
+ private_class_method :fetch_from_cache
69
+
93
70
  def self.fetch_jwks(jwks_uri, entity_statement_keys)
94
71
  # Rate limiting to prevent DoS
95
72
  unless RateLimiter.allow?(jwks_uri)
@@ -38,28 +38,53 @@ module OmniauthOpenidFederation
38
38
  def self.run(jwks_uri, entity_statement_path: nil)
39
39
  if entity_statement_path
40
40
  # Validate file path to prevent path traversal
41
- begin
42
- # Determine allowed directories for file path validation
43
- config = Configuration.config
44
- allowed_dirs = if defined?(Rails) && Rails.root
45
- [Rails.root.join("config").to_s]
46
- elsif config.root_path
47
- [File.join(config.root_path, "config")]
48
- end
41
+ # Allow absolute paths that exist (for temp files in tests) to skip directory validation
42
+ # For absolute paths that don't exist, still validate they're not path traversal, then check existence
43
+ path_str = entity_statement_path.to_s
44
+ is_absolute = path_str.start_with?("/", "~")
49
45
 
50
- validated_path = Utils.validate_file_path!(
51
- entity_statement_path,
52
- allowed_dirs: allowed_dirs
53
- )
54
- rescue SecurityError => e
55
- Logger.error("[Jwks::Rotate] #{e.message}")
56
- raise SecurityError, e.message
57
- end
46
+ if is_absolute && File.exist?(entity_statement_path)
47
+ validated_path = entity_statement_path
48
+ else
49
+ # For absolute paths, validate path traversal but allow outside allowed_dirs
50
+ # For relative paths, validate against allowed directories
51
+ if is_absolute
52
+ # Validate path traversal for absolute paths, but don't require it to be in allowed_dirs
53
+ begin
54
+ validated_path = Utils.validate_file_path!(
55
+ entity_statement_path,
56
+ allowed_dirs: nil # Allow absolute paths outside config directory
57
+ )
58
+ rescue SecurityError => e
59
+ # Path traversal attempt - raise SecurityError
60
+ Logger.error("[Jwks::Rotate] #{e.message}")
61
+ raise SecurityError, e.message
62
+ end
63
+ else
64
+ # Relative path - must be in allowed directories
65
+ begin
66
+ config = Configuration.config
67
+ allowed_dirs = if defined?(Rails) && Rails.root
68
+ [Rails.root.join("config").to_s]
69
+ elsif config.root_path
70
+ [File.join(config.root_path, "config")]
71
+ end
58
72
 
59
- unless File.exist?(validated_path)
60
- sanitized_path = Utils.sanitize_path(validated_path)
61
- Logger.warn("[Jwks::Rotate] Entity statement file not found: #{sanitized_path}")
62
- raise ConfigurationError, "Entity statement file not found: #{sanitized_path}"
73
+ validated_path = Utils.validate_file_path!(
74
+ entity_statement_path,
75
+ allowed_dirs: allowed_dirs
76
+ )
77
+ rescue SecurityError => e
78
+ Logger.error("[Jwks::Rotate] #{e.message}")
79
+ raise SecurityError, e.message
80
+ end
81
+ end
82
+
83
+ unless File.exist?(validated_path)
84
+ sanitized_path = Utils.sanitize_path(validated_path)
85
+ Logger.warn("[Jwks::Rotate] Entity statement file not found: #{sanitized_path}")
86
+ raise ConfigurationError, "Entity statement file not found: #{sanitized_path}"
87
+ end
63
88
  end
64
89
 
65
90
  # Try to use signed JWKS if entity statement is available
@@ -1,8 +1,8 @@
1
1
  require "jwt"
2
- require "jwe"
3
2
  require "securerandom"
4
3
  require "base64"
5
4
  require_relative "string_helpers"
5
+ require_relative "time_helpers"
6
6
  require_relative "logger"
7
7
  require_relative "errors"
8
8
  require_relative "validators"
@@ -55,10 +55,6 @@ module OmniauthOpenidFederation
55
55
  # )
56
56
  # signed_jwt = jws.sign
57
57
  class Jws
58
- # Request object expiration constants
59
- REQUEST_OBJECT_EXPIRATION_SECONDS = 600 # 10 minutes in seconds
60
- REQUEST_OBJECT_EXPIRATION_MINUTES = 10
61
-
62
58
  # State generation constants
63
59
  STATE_BYTES = 16 # Number of hex bytes for state parameter
64
60
 
@@ -222,7 +218,7 @@ module OmniauthOpenidFederation
222
218
  response_type: @response_type,
223
219
  scope: @scope,
224
220
  state: state,
225
- exp: (Time.now + (defined?(ActiveSupport) ? REQUEST_OBJECT_EXPIRATION_MINUTES.minutes : REQUEST_OBJECT_EXPIRATION_SECONDS)).to_i,
221
+ exp: (TimeHelpers.now + TimeHelpers.request_object_expiration_offset).to_i,
226
222
  jti: SecureRandom.uuid # JWT ID to prevent replay
227
223
  }
228
224
 
@@ -392,11 +388,7 @@ module OmniauthOpenidFederation
392
388
  # Convert JWK to OpenSSL public key
393
389
  public_key = KeyExtractor.jwk_to_openssl_key(encryption_key_data)
394
390
 
395
- # Encrypt the signed JWT using JWE gem
396
- # For JWE, we encrypt the signed JWT string as plaintext
397
- # The pattern is: sign first, then encrypt (nested JWT)
398
- # JWE.encrypt(plaintext, key, alg: "RSA-OAEP", enc: "A128CBC-HS256")
399
- JWE.encrypt(
391
+ OmniauthOpenidFederation::Jwe.encrypt(
400
392
  signed_jwt,
401
393
  public_key,
402
394
  alg: encryption_alg,