omniauth_openid_federation 1.3.0 → 1.3.2

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: bdb5dbe8832e2837c2970556b8498492c8e32489e8efad28eff176be94af1a1d
4
- data.tar.gz: 7addf88091e26b0c603a5bd50fe8c701d8d8cb2c5a5854549a59795b7782a792
3
+ metadata.gz: 8f8bba8b102bcf8c24fdd775d5d898d92b27c27d8e6df40c63ff23756aee4f8d
4
+ data.tar.gz: 900a1851f0a9917b5593f6146d965271180a979d312b9ff5fa15c295578172e5
5
5
  SHA512:
6
- metadata.gz: 31a2b0f6042da80bcb445c0d92e137b06da8d017e58e05835fb0983e35df4215797ccabef024378cc5faa78006037f0f338cb074bc8784c72eea02b349fbc248
7
- data.tar.gz: cd47a106c42a20a80caa3db3212dbe4c1748af65a17f88b4a57273f35a01cbc79e814b998f793691b50b368592f5d31ac31ff87c1831880497047b02178569d1
6
+ metadata.gz: 31a7bcc8b8e1661dc9bcc13604e2e92279d81501e0a1da25b8b48a6f9bf35251489720aa1879b6beeb89de32bd2970d72c086e58b1be43d0f4c125809339fb51
7
+ data.tar.gz: 5c27ea824e18a49a0fe899f6f6f2ade3c6a10872ade9b2eca3690cada27e06aa5ada813f1cdf34763881bc8c4ee5d1033f0a7ed821a8378602ee72db169150f7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.3.2 (2025-12-09)
4
+
5
+ - Added `TimeHelpers` module for compatibility with non-Rails environments
6
+ - Replaced `Time.zone` usage with `TimeHelpers` to work with or without ActiveSupport
7
+
8
+ ## 1.3.1 (2025-12-09)
9
+
10
+ - Enhanced SSL configuration for HTTPS requests in tasks_helper.rb
11
+ - Updated federation controller to use ApplicationController
12
+ - Updated routes to have semaphore if it is already loaded
13
+ - Updated gemfiles and workflows for Rails 8 compatibility
14
+ - Improved time handling in integration and mock server classes using Time.zone.now
15
+
3
16
  ## 1.3.0 (2025-11-28)
4
17
 
5
18
  - Added `prepare_request_object_params` proc option to customize request parameters before signing
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # omniauth_openid_federation
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/omniauth_openid_federation.svg?v=1.1.0)](https://badge.fury.io/rb/omniauth_openid_federation) [![Test Status](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb/graph/badge.svg?token=CX3O9M1GIT)](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb/graph/badge.svg)
3
+ [![Gem Version](https://badge.fury.io/rb/omniauth_openid_federation.svg?v=1.1.0)](https://badge.fury.io/rb/omniauth_openid_federation) [![Test Status](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb/graph/badge.svg?token=CX3O9M1GIT)](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=amkisko_omniauth_openid_federation.rb&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=amkisko_omniauth_openid_federation.rb)
4
4
 
5
5
  OmniAuth strategy for OpenID Federation providers with comprehensive security features, supporting signed request objects, ID token encryption, and full OpenID Federation 1.0 compliance.
6
6
 
@@ -12,7 +12,7 @@
12
12
  require "omniauth_openid_federation/cache_adapter"
13
13
 
14
14
  module OmniauthOpenidFederation
15
- class FederationController < ActionController::Base
15
+ class FederationController < ::ApplicationController
16
16
  # Serve the entity statement
17
17
  #
18
18
  # GET /.well-known/openid-federation
data/config/routes.rb CHANGED
@@ -1,17 +1,27 @@
1
1
  # Routes for OpenID Federation well-known endpoints
2
2
  # These routes are mounted at the root level (not namespaced) because
3
3
  # OpenID Federation spec requires specific well-known paths
4
- OmniauthOpenidFederation::Engine.routes.draw do
5
- # OpenID Federation 1.0 Section 9: Entity Configuration endpoint
6
- # MUST be at /.well-known/openid-federation
7
- get "/.well-known/openid-federation", to: "omniauth_openid_federation/federation#show", as: :openid_federation
4
+ # Guard to prevent double-loading routes (important for test isolation)
5
+ # Use a file-level instance variable that doesn't trigger class loading
6
+ @_omniauth_openid_federation_routes_loaded ||= false
7
+ unless @_omniauth_openid_federation_routes_loaded
8
+ @_omniauth_openid_federation_routes_loaded = true
9
+ begin
10
+ OmniauthOpenidFederation::Engine.routes.draw do
11
+ # OpenID Federation 1.0 Section 9: Entity Configuration endpoint
12
+ # MUST be at /.well-known/openid-federation
13
+ get "/.well-known/openid-federation", to: "omniauth_openid_federation/federation#show", as: :openid_federation
8
14
 
9
- # Fetch endpoint for Subordinate Statements (Section 6.1)
10
- get "/.well-known/openid-federation/fetch", to: "omniauth_openid_federation/federation#fetch", as: :openid_federation_fetch
15
+ # Fetch endpoint for Subordinate Statements (Section 6.1)
16
+ get "/.well-known/openid-federation/fetch", to: "omniauth_openid_federation/federation#fetch", as: :openid_federation_fetch
11
17
 
12
- # Standard JWKS endpoint
13
- get "/.well-known/jwks.json", to: "omniauth_openid_federation/federation#jwks", as: :openid_federation_jwks
18
+ # Standard JWKS endpoint
19
+ get "/.well-known/jwks.json", to: "omniauth_openid_federation/federation#jwks", as: :openid_federation_jwks
14
20
 
15
- # Signed JWKS endpoint (OpenID Federation requirement)
16
- get "/.well-known/signed-jwks.json", to: "omniauth_openid_federation/federation#signed_jwks", as: :openid_federation_signed_jwks
21
+ # Signed JWKS endpoint (OpenID Federation requirement)
22
+ get "/.well-known/signed-jwks.json", to: "omniauth_openid_federation/federation#signed_jwks", as: :openid_federation_signed_jwks
23
+ end
24
+ rescue NameError, LoadError
25
+ # Rails not available or not fully initialized - routes will be loaded when Engine initializes
26
+ end
17
27
  end
@@ -319,7 +319,7 @@ class IntegrationTestFlow
319
319
  attempt = 0
320
320
  ready = false
321
321
  server_name = url.include?("9292") ? "OP" : "RP"
322
- start_time = Time.now
322
+ start_time = Time.zone.now
323
323
 
324
324
  while attempt < max_attempts && !ready
325
325
  begin
@@ -330,7 +330,7 @@ class IntegrationTestFlow
330
330
  response = http.get(uri.path)
331
331
 
332
332
  if response.code == "200"
333
- elapsed = (Time.now - start_time).round(1)
333
+ elapsed = (Time.zone.now - start_time).round(1)
334
334
  puts " ✓ #{url} is ready (#{elapsed}s)"
335
335
  ready = true
336
336
  end
@@ -342,7 +342,7 @@ class IntegrationTestFlow
342
342
  attempt += 1
343
343
  # Show progress every 5 attempts (1 second)
344
344
  if attempt % 5 == 0
345
- elapsed = (Time.now - start_time).round(1)
345
+ elapsed = (Time.zone.now - start_time).round(1)
346
346
  print "."
347
347
  end
348
348
  sleep check_interval
@@ -350,7 +350,7 @@ class IntegrationTestFlow
350
350
  end
351
351
 
352
352
  unless ready
353
- elapsed = (Time.now - start_time).round(1)
353
+ elapsed = (Time.zone.now - start_time).round(1)
354
354
  puts "\n ✗ #{server_name} server at #{url} did not become ready in time (#{elapsed}s)"
355
355
  err_log = File.join(@tmp_dir, "#{server_name.downcase}_server_error.log")
356
356
  log_file = File.join(@tmp_dir, "#{server_name.downcase}_server.log")
@@ -111,7 +111,7 @@ class MockOPServer
111
111
  end
112
112
 
113
113
  def self.load_signing_key(key_data)
114
- if key_data.nil? || key_data.empty?
114
+ if key_data.blank?
115
115
  # Generate a new key for testing
116
116
  OpenSSL::PKey::RSA.new(2048)
117
117
  elsif key_data.is_a?(String)
@@ -126,7 +126,7 @@ class MockOPServer
126
126
  end
127
127
 
128
128
  def self.load_encryption_key(key_data)
129
- return nil if key_data.nil? || key_data.empty?
129
+ return nil if key_data.blank?
130
130
  load_signing_key(key_data)
131
131
  end
132
132
 
@@ -478,7 +478,7 @@ class MockOPServer
478
478
  redirect_uri: redirect_uri,
479
479
  state: state,
480
480
  nonce: nonce,
481
- created_at: Time.now
481
+ created_at: Time.zone.now
482
482
  }
483
483
 
484
484
  # Redirect back to RP with authorization code
@@ -66,7 +66,7 @@ class MockRPServer
66
66
  end
67
67
 
68
68
  def self.load_signing_key(key_data)
69
- if key_data.nil? || key_data.empty?
69
+ if key_data.blank?
70
70
  OpenSSL::PKey::RSA.new(2048)
71
71
  elsif key_data.is_a?(String)
72
72
  if key_data.include?("BEGIN")
@@ -80,7 +80,7 @@ class MockRPServer
80
80
  end
81
81
 
82
82
  def self.load_encryption_key(key_data)
83
- return nil if key_data.nil? || key_data.empty?
83
+ return nil if key_data.blank?
84
84
  load_signing_key(key_data)
85
85
  end
86
86
 
@@ -297,7 +297,7 @@ class MockRPServer
297
297
  provider_entity_id: provider_entity_id,
298
298
  redirect_uri: redirect_uri,
299
299
  nonce: nonce,
300
- created_at: Time.now
300
+ created_at: Time.zone.now
301
301
  }
302
302
 
303
303
  # Step 5: Redirect to provider
@@ -5,6 +5,7 @@ require_relative "key_extractor"
5
5
  require_relative "utils"
6
6
  require_relative "configuration"
7
7
  require_relative "logger"
8
+ require_relative "string_helpers"
8
9
 
9
10
  # Entity Statement Reader for OpenID Federation 1.0
10
11
  # @see https://openid.net/specs/openid-federation-1_0.html OpenID Federation 1.0 Specification
@@ -30,7 +31,7 @@ module OmniauthOpenidFederation
30
31
  # @return [Array<Hash>] Array of JWK hash objects
31
32
  def fetch_keys(entity_statement_path: nil)
32
33
  entity_statement = load_entity_statement(entity_statement_path)
33
- return [] if entity_statement.nil? || entity_statement.empty?
34
+ return [] if StringHelpers.blank?(entity_statement)
34
35
 
35
36
  # Decode self-signed entity statement
36
37
  # Entity statements are self-signed, so we validate using their own JWKS
@@ -53,7 +54,7 @@ module OmniauthOpenidFederation
53
54
  # @return [Hash, nil] Hash with provider metadata or nil if not found
54
55
  def parse_metadata(entity_statement_path: nil)
55
56
  entity_statement = load_entity_statement(entity_statement_path)
56
- return nil if entity_statement.nil? || entity_statement.empty?
57
+ return nil if StringHelpers.blank?(entity_statement)
57
58
 
58
59
  # Decode JWT payload
59
60
  jwt_parts = entity_statement.split(".")
@@ -93,21 +94,45 @@ module OmniauthOpenidFederation
93
94
  def load_entity_statement(entity_statement_path)
94
95
  return nil if entity_statement_path.nil? || entity_statement_path.to_s.empty?
95
96
 
96
- # Determine allowed directories for file path validation
97
- config = OmniauthOpenidFederation::Configuration.config
98
- allowed_dirs = if defined?(Rails) && Rails.root
99
- [Rails.root.join("config").to_s]
100
- elsif config.root_path
101
- [File.join(config.root_path, "config")]
97
+ # If path is absolute and exists, allow it (for temp files in tests)
98
+ # For absolute paths that don't exist, validate path traversal but allow outside allowed_dirs
99
+ # For relative paths, validate against allowed directories
100
+ path_str = entity_statement_path.to_s
101
+ is_absolute = path_str.start_with?("/", "~")
102
+
103
+ if is_absolute && File.exist?(entity_statement_path)
104
+ validated_path = entity_statement_path
105
+ elsif is_absolute
106
+ # Absolute path - validate path traversal but allow outside allowed_dirs
107
+ begin
108
+ validated_path = Utils.validate_file_path!(
109
+ entity_statement_path,
110
+ allowed_dirs: nil # Allow absolute paths outside config directory
111
+ )
112
+ rescue SecurityError
113
+ return nil
114
+ end
115
+ else
116
+ # Relative path - must be in allowed directories
117
+ config = OmniauthOpenidFederation::Configuration.config
118
+ allowed_dirs = if defined?(Rails) && Rails.root
119
+ [Rails.root.join("config").to_s]
120
+ elsif config.root_path
121
+ [File.join(config.root_path, "config")]
122
+ end
123
+
124
+ begin
125
+ # Validate file path to prevent path traversal attacks
126
+ validated_path = Utils.validate_file_path!(
127
+ entity_statement_path,
128
+ allowed_dirs: allowed_dirs
129
+ )
130
+ rescue SecurityError
131
+ return nil
132
+ end
102
133
  end
103
134
 
104
135
  begin
105
- # Validate file path to prevent path traversal attacks
106
- validated_path = Utils.validate_file_path!(
107
- entity_statement_path,
108
- allowed_dirs: allowed_dirs
109
- )
110
-
111
136
  return nil unless File.exist?(validated_path)
112
137
 
113
138
  File.read(validated_path)
@@ -4,6 +4,7 @@ require "openssl"
4
4
  require "time"
5
5
  require_relative "../logger"
6
6
  require_relative "../errors"
7
+ require_relative "../string_helpers"
7
8
 
8
9
  # Entity Statement Builder for OpenID Federation 1.0
9
10
  # @see https://openid.net/specs/openid-federation-1_0.html OpenID Federation 1.0 Specification
@@ -82,7 +83,6 @@ module OmniauthOpenidFederation
82
83
 
83
84
  payload = build_payload
84
85
 
85
- # Build JWT header
86
86
  # Per OpenID Federation 1.0 Section 3.1: typ MUST be "entity-statement+jwt"
87
87
  header = {
88
88
  alg: "RS256",
@@ -102,13 +102,13 @@ module OmniauthOpenidFederation
102
102
  private
103
103
 
104
104
  def validate_parameters
105
- raise ConfigurationError, "Issuer is required" if @issuer.nil? || @issuer.empty?
106
- raise ConfigurationError, "Subject is required" if @subject.nil? || @subject.empty?
105
+ raise ConfigurationError, "Issuer is required" if StringHelpers.blank?(@issuer)
106
+ raise ConfigurationError, "Subject is required" if StringHelpers.blank?(@subject)
107
107
  raise ConfigurationError, "Private key is required" if @private_key.nil?
108
- raise ConfigurationError, "JWKS is required" if @jwks.nil? || @jwks.empty?
109
- raise ConfigurationError, "Metadata is required" if @metadata.nil? || @metadata.empty?
110
- raise ConfigurationError, "JWKS must contain at least one key" if @jwks["keys"].nil? || @jwks["keys"].empty?
111
- raise ConfigurationError, "Key ID (kid) is required" if @kid.nil? || @kid.empty?
108
+ raise ConfigurationError, "JWKS is required" if StringHelpers.blank?(@jwks)
109
+ raise ConfigurationError, "Metadata is required" if StringHelpers.blank?(@metadata)
110
+ raise ConfigurationError, "JWKS must contain at least one key" if StringHelpers.blank?(@jwks["keys"])
111
+ raise ConfigurationError, "Key ID (kid) is required" if StringHelpers.blank?(@kid)
112
112
  end
113
113
 
114
114
  def build_payload
@@ -125,7 +125,6 @@ module OmniauthOpenidFederation
125
125
  metadata: @metadata
126
126
  }
127
127
 
128
- # Entity Configuration specific claims
129
128
  if is_entity_configuration
130
129
  payload[:authority_hints] = @authority_hints if @authority_hints
131
130
  payload[:trust_marks] = @trust_marks if @trust_marks
@@ -133,7 +132,6 @@ module OmniauthOpenidFederation
133
132
  payload[:trust_mark_owners] = @trust_mark_owners if @trust_mark_owners
134
133
  end
135
134
 
136
- # Subordinate Statement specific claims
137
135
  if is_subordinate_statement
138
136
  payload[:metadata_policy] = @metadata_policy if @metadata_policy
139
137
  payload[:metadata_policy_crit] = @metadata_policy_crit if @metadata_policy_crit
@@ -141,21 +139,17 @@ module OmniauthOpenidFederation
141
139
  payload[:source_endpoint] = @source_endpoint if @source_endpoint
142
140
  end
143
141
 
144
- # Common optional claims
145
142
  payload[:crit] = @crit if @crit
146
143
 
147
144
  payload
148
145
  end
149
146
 
150
147
  def normalize_jwks(jwks)
151
- # Ensure JWKS is a hash with "keys" array
152
148
  if jwks.is_a?(Hash)
153
- # If it has :keys or "keys", use as-is
154
149
  if jwks.key?(:keys) || jwks.key?("keys")
155
150
  keys = jwks[:keys] || jwks["keys"]
156
151
  {"keys" => normalize_keys(keys)}
157
152
  else
158
- # If it's just a hash, wrap it
159
153
  {"keys" => [jwks]}
160
154
  end
161
155
  elsif jwks.is_a?(Array)
@@ -168,7 +162,6 @@ module OmniauthOpenidFederation
168
162
  def normalize_keys(keys)
169
163
  keys.map do |key|
170
164
  if key.is_a?(Hash)
171
- # Convert symbol keys to string keys
172
165
  key.transform_keys(&:to_s)
173
166
  else
174
167
  key
@@ -17,18 +17,47 @@ module OmniauthOpenidFederation
17
17
  # @raise [ValidationError] If parsing fails
18
18
  def self.parse_for_signed_jwks(entity_statement_path)
19
19
  # Determine allowed directories for file path validation
20
- config = Configuration.config
21
- allowed_dirs = if defined?(Rails) && Rails.root
22
- [Rails.root.join("config").to_s]
23
- elsif config.root_path
24
- [File.join(config.root_path, "config")]
25
- end
20
+ # If path is absolute and exists, allow it (for temp files in tests)
21
+ # For absolute paths that don't exist, validate path traversal but allow outside allowed_dirs
22
+ # For relative paths, validate against allowed directories
23
+ path_str = entity_statement_path.to_s
24
+ is_absolute = path_str.start_with?("/", "~")
25
+
26
+ if is_absolute && File.exist?(entity_statement_path)
27
+ validated_path = entity_statement_path
28
+ elsif is_absolute
29
+ # Absolute path - validate path traversal but allow outside allowed_dirs
30
+ begin
31
+ validated_path = Utils.validate_file_path!(
32
+ entity_statement_path,
33
+ allowed_dirs: nil # Allow absolute paths outside config directory
34
+ )
35
+ rescue SecurityError => e
36
+ OmniauthOpenidFederation::Logger.warn("[EntityStatementHelper] Security error: #{e.message}")
37
+ return nil
38
+ end
39
+ else
40
+ # Relative path - must be in allowed directories
41
+ config = Configuration.config
42
+ allowed_dirs = if defined?(Rails) && Rails.root
43
+ [Rails.root.join("config").to_s]
44
+ elsif config.root_path
45
+ [File.join(config.root_path, "config")]
46
+ end
26
47
 
27
- # Validate file path to prevent path traversal
28
- validated_path = Utils.validate_file_path!(
29
- entity_statement_path,
30
- allowed_dirs: allowed_dirs
31
- )
48
+ begin
49
+ # Validate file path to prevent path traversal
50
+ validated_path = Utils.validate_file_path!(
51
+ entity_statement_path,
52
+ allowed_dirs: allowed_dirs
53
+ )
54
+ rescue SecurityError => e
55
+ # For relative paths with path traversal, raise SecurityError instead of returning nil
56
+ # This is a security violation that should be explicitly handled
57
+ OmniauthOpenidFederation::Logger.warn("[EntityStatementHelper] Security error: #{e.message}")
58
+ raise SecurityError, e.message
59
+ end
60
+ end
32
61
 
33
62
  unless File.exist?(validated_path)
34
63
  sanitized_path = Utils.sanitize_path(validated_path)