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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +1 -1
- data/app/controllers/omniauth_openid_federation/federation_controller.rb +1 -1
- data/config/routes.rb +20 -10
- data/examples/integration_test_flow.rb +4 -4
- data/examples/mock_op_server.rb +3 -3
- data/examples/mock_rp_server.rb +3 -3
- data/lib/omniauth_openid_federation/entity_statement_reader.rb +39 -14
- data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +7 -14
- data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +40 -11
- data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +6 -87
- data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +3 -15
- data/lib/omniauth_openid_federation/federation_endpoint.rb +39 -171
- data/lib/omniauth_openid_federation/jwks/rotate.rb +45 -20
- data/lib/omniauth_openid_federation/jws.rb +2 -1
- data/lib/omniauth_openid_federation/rack_endpoint.rb +19 -7
- data/lib/omniauth_openid_federation/tasks_helper.rb +23 -5
- data/lib/omniauth_openid_federation/time_helpers.rb +60 -0
- data/lib/omniauth_openid_federation/utils.rb +4 -7
- data/lib/omniauth_openid_federation/validators.rb +12 -36
- data/lib/omniauth_openid_federation/version.rb +1 -1
- data/lib/omniauth_openid_federation.rb +1 -0
- data/lib/tasks/omniauth_openid_federation.rake +4 -3
- data/sig/omniauth_openid_federation.rbs +6 -0
- metadata +100 -1
|
@@ -116,7 +116,7 @@ module OmniauthOpenidFederation
|
|
|
116
116
|
subject_entity_id = request.params["sub"]
|
|
117
117
|
|
|
118
118
|
unless subject_entity_id
|
|
119
|
-
return error_response(400, {error: "invalid_request", error_description: "Missing required parameter: sub"}
|
|
119
|
+
return error_response(400, {error: "invalid_request", error_description: "Missing required parameter: sub"})
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
# Security: Validate entity identifier per OpenID Federation 1.0 spec
|
|
@@ -125,22 +125,22 @@ module OmniauthOpenidFederation
|
|
|
125
125
|
# Validate and get trimmed value
|
|
126
126
|
subject_entity_id = OmniauthOpenidFederation::Validators.validate_entity_identifier!(subject_entity_id)
|
|
127
127
|
rescue SecurityError => e
|
|
128
|
-
return error_response(400, {error: "invalid_request", error_description: "Invalid subject entity ID: #{e.message}"}
|
|
128
|
+
return error_response(400, {error: "invalid_request", error_description: "Invalid subject entity ID: #{e.message}"})
|
|
129
129
|
rescue => e
|
|
130
|
-
return error_response(400, {error: "invalid_request", error_description: "Subject entity ID validation failed: #{e.message}"}
|
|
130
|
+
return error_response(400, {error: "invalid_request", error_description: "Subject entity ID validation failed: #{e.message}"})
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
# Validate that subject is not the issuer (invalid request per spec)
|
|
134
134
|
config = OmniauthOpenidFederation::FederationEndpoint.configuration
|
|
135
135
|
if subject_entity_id == config.issuer
|
|
136
|
-
return error_response(400, {error: "invalid_request", error_description: "Subject cannot be the issuer"}
|
|
136
|
+
return error_response(400, {error: "invalid_request", error_description: "Subject cannot be the issuer"})
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
# Get Subordinate Statement
|
|
140
140
|
subordinate_statement = OmniauthOpenidFederation::FederationEndpoint.get_subordinate_statement(subject_entity_id)
|
|
141
141
|
|
|
142
142
|
unless subordinate_statement
|
|
143
|
-
return error_response(404, {error: "not_found", error_description: "Subordinate Statement not found for subject: #{subject_entity_id}"}
|
|
143
|
+
return error_response(404, {error: "not_found", error_description: "Subordinate Statement not found for subject: #{subject_entity_id}"})
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
headers = {
|
|
@@ -188,11 +188,23 @@ module OmniauthOpenidFederation
|
|
|
188
188
|
# Return error response
|
|
189
189
|
#
|
|
190
190
|
# @param status [Integer] HTTP status code
|
|
191
|
-
# @param message [String] Error message
|
|
191
|
+
# @param message [String, Hash] Error message (string) or error hash (will be converted to JSON)
|
|
192
192
|
# @return [Array] Rack response
|
|
193
193
|
def error_response(status, message)
|
|
194
194
|
content_type = (status == 503) ? "text/plain" : "application/json"
|
|
195
|
-
body =
|
|
195
|
+
body = if status == 503
|
|
196
|
+
message
|
|
197
|
+
elsif message.is_a?(Hash)
|
|
198
|
+
message.to_json
|
|
199
|
+
else
|
|
200
|
+
# If message is already JSON string, parse and re-encode to ensure proper format
|
|
201
|
+
begin
|
|
202
|
+
parsed = JSON.parse(message)
|
|
203
|
+
parsed.to_json
|
|
204
|
+
rescue JSON::ParserError
|
|
205
|
+
{error: message}.to_json
|
|
206
|
+
end
|
|
207
|
+
end
|
|
196
208
|
|
|
197
209
|
[status, {"Content-Type" => content_type}, [body]]
|
|
198
210
|
end
|
|
@@ -285,7 +285,19 @@ module OmniauthOpenidFederation
|
|
|
285
285
|
end
|
|
286
286
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
287
287
|
http.use_ssl = (uri.scheme == "https")
|
|
288
|
-
|
|
288
|
+
if uri.scheme == "https"
|
|
289
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
290
|
+
|
|
291
|
+
# Set ca_file directly - this is the simplest and most reliable approach
|
|
292
|
+
# Try SSL_CERT_FILE first, then default cert file
|
|
293
|
+
ca_file = if ENV["SSL_CERT_FILE"] && File.file?(ENV["SSL_CERT_FILE"])
|
|
294
|
+
ENV["SSL_CERT_FILE"]
|
|
295
|
+
elsif File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE)
|
|
296
|
+
OpenSSL::X509::DEFAULT_CERT_FILE
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
http.ca_file = ca_file if ca_file
|
|
300
|
+
end
|
|
289
301
|
|
|
290
302
|
request_path = uri.path
|
|
291
303
|
request_path += "?#{uri.query}" if uri.query
|
|
@@ -466,6 +478,9 @@ module OmniauthOpenidFederation
|
|
|
466
478
|
}
|
|
467
479
|
|
|
468
480
|
# HTTP client helper for custom requests
|
|
481
|
+
build_http_client = lambda do |connect_timeout: 10, read_timeout: 10|
|
|
482
|
+
HTTP.timeout(connect: connect_timeout, read: read_timeout)
|
|
483
|
+
end
|
|
469
484
|
|
|
470
485
|
# Step 1: Fetch login page for CSRF token and cookies
|
|
471
486
|
results[:steps_completed] << "fetch_csrf_token"
|
|
@@ -476,7 +491,8 @@ module OmniauthOpenidFederation
|
|
|
476
491
|
cookies = []
|
|
477
492
|
|
|
478
493
|
begin
|
|
479
|
-
|
|
494
|
+
http_client = build_http_client.call(connect_timeout: 10, read_timeout: 10)
|
|
495
|
+
login_response = http_client.get(login_page_url)
|
|
480
496
|
|
|
481
497
|
unless login_response.status.success?
|
|
482
498
|
raise "Failed to fetch login page: #{login_response.status.code} #{login_response.status.reason}"
|
|
@@ -578,7 +594,8 @@ module OmniauthOpenidFederation
|
|
|
578
594
|
common_paths.each do |path|
|
|
579
595
|
test_url = URI.join(base_url, path).to_s
|
|
580
596
|
begin
|
|
581
|
-
|
|
597
|
+
http_client = build_http_client.call(connect_timeout: 5, read_timeout: 5)
|
|
598
|
+
test_response = http_client.get(test_url)
|
|
582
599
|
if test_response.status.code >= 300 && test_response.status.code < 400
|
|
583
600
|
auth_endpoint = test_url
|
|
584
601
|
break
|
|
@@ -613,7 +630,8 @@ module OmniauthOpenidFederation
|
|
|
613
630
|
# Include acr_values if provided (must be configured in request_object_params to be included in JWT)
|
|
614
631
|
form_data[:acr_values] = provider_acr if StringHelpers.present?(provider_acr)
|
|
615
632
|
|
|
616
|
-
|
|
633
|
+
http_client = build_http_client.call(connect_timeout: 10, read_timeout: 10)
|
|
634
|
+
auth_response = http_client
|
|
617
635
|
.headers(headers)
|
|
618
636
|
.post(auth_endpoint, form: form_data)
|
|
619
637
|
|
|
@@ -692,7 +710,7 @@ module OmniauthOpenidFederation
|
|
|
692
710
|
require "cgi"
|
|
693
711
|
require "json"
|
|
694
712
|
require "base64"
|
|
695
|
-
require_relative "
|
|
713
|
+
require_relative "strategy"
|
|
696
714
|
|
|
697
715
|
results = {
|
|
698
716
|
steps_completed: [],
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
end
|
|
60
|
+
end
|
|
@@ -1,4 +1,6 @@
|
|
|
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
|
|
@@ -18,7 +20,7 @@ module OmniauthOpenidFederation
|
|
|
18
20
|
# @param path [String, nil] The file path
|
|
19
21
|
# @return [String] Sanitized path (filename only)
|
|
20
22
|
def self.sanitize_path(path)
|
|
21
|
-
return "[REDACTED]" if
|
|
23
|
+
return "[REDACTED]" if StringHelpers.blank?(path)
|
|
22
24
|
File.basename(path)
|
|
23
25
|
end
|
|
24
26
|
|
|
@@ -27,7 +29,7 @@ module OmniauthOpenidFederation
|
|
|
27
29
|
# @param uri [String, nil] The URI
|
|
28
30
|
# @return [String] Sanitized URI
|
|
29
31
|
def self.sanitize_uri(uri)
|
|
30
|
-
return "[REDACTED]" if
|
|
32
|
+
return "[REDACTED]" if StringHelpers.blank?(uri)
|
|
31
33
|
begin
|
|
32
34
|
parsed = URI.parse(uri)
|
|
33
35
|
"#{parsed.scheme}://#{parsed.host}/[REDACTED]"
|
|
@@ -70,16 +72,13 @@ module OmniauthOpenidFederation
|
|
|
70
72
|
def self.validate_file_path!(path, allowed_dirs: nil)
|
|
71
73
|
raise SecurityError, "File path cannot be nil" if path.nil?
|
|
72
74
|
|
|
73
|
-
# Convert Pathname to string if needed
|
|
74
75
|
path_str = path.to_s
|
|
75
76
|
raise SecurityError, "File path cannot be empty" if path_str.empty?
|
|
76
77
|
|
|
77
|
-
# Check for path traversal attempts
|
|
78
78
|
if path_str.include?("..") || path_str.include?("~")
|
|
79
79
|
raise SecurityError, "Path traversal detected in: #{sanitize_path(path_str)}"
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
# Resolve to absolute path
|
|
83
82
|
resolved = File.expand_path(path_str)
|
|
84
83
|
|
|
85
84
|
# Validate it's within allowed directories if specified
|
|
@@ -120,7 +119,6 @@ module OmniauthOpenidFederation
|
|
|
120
119
|
n = Base64.urlsafe_encode64(key.n.to_s(2), padding: false)
|
|
121
120
|
e = Base64.urlsafe_encode64(key.e.to_s(2), padding: false)
|
|
122
121
|
|
|
123
|
-
# Generate kid (key ID) from public key
|
|
124
122
|
public_key_pem = key.public_key.to_pem
|
|
125
123
|
kid = Digest::SHA256.hexdigest(public_key_pem)[0, 16]
|
|
126
124
|
|
|
@@ -131,7 +129,6 @@ module OmniauthOpenidFederation
|
|
|
131
129
|
e: e
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
# Add use field if specified
|
|
135
132
|
jwk[:use] = use if use
|
|
136
133
|
|
|
137
134
|
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
|
|
@@ -13,7 +14,6 @@ module OmniauthOpenidFederation
|
|
|
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
17
|
if private_key.is_a?(String)
|
|
18
18
|
begin
|
|
19
19
|
OpenSSL::PKey::RSA.new(private_key)
|
|
@@ -82,10 +82,8 @@ module OmniauthOpenidFederation
|
|
|
82
82
|
def self.validate_client_options!(client_options)
|
|
83
83
|
client_options ||= {}
|
|
84
84
|
|
|
85
|
-
# Normalize hash keys to symbols
|
|
86
85
|
normalized = normalize_hash(client_options)
|
|
87
86
|
|
|
88
|
-
# Validate required fields (structure validation, not security)
|
|
89
87
|
if StringHelpers.blank?(normalized[:identifier])
|
|
90
88
|
raise ConfigurationError, "Client identifier is required"
|
|
91
89
|
end
|
|
@@ -94,8 +92,6 @@ module OmniauthOpenidFederation
|
|
|
94
92
|
raise ConfigurationError, "Redirect URI is required"
|
|
95
93
|
end
|
|
96
94
|
|
|
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
95
|
begin
|
|
100
96
|
parsed = URI.parse(normalized[:redirect_uri].to_s)
|
|
101
97
|
unless parsed.is_a?(URI::HTTP) || parsed.is_a?(URI::HTTPS)
|
|
@@ -105,10 +101,8 @@ module OmniauthOpenidFederation
|
|
|
105
101
|
raise ConfigurationError, "Invalid redirect URI format: #{e.message}"
|
|
106
102
|
end
|
|
107
103
|
|
|
108
|
-
# Validate private key (required for security)
|
|
109
104
|
validate_private_key!(normalized[:private_key])
|
|
110
105
|
|
|
111
|
-
# Basic format check for endpoints (config validation, not security)
|
|
112
106
|
%i[authorization_endpoint token_endpoint jwks_uri].each do |endpoint|
|
|
113
107
|
if normalized.key?(endpoint) && !StringHelpers.blank?(normalized[endpoint])
|
|
114
108
|
# Endpoints can be paths or full URLs
|
|
@@ -145,7 +139,6 @@ module OmniauthOpenidFederation
|
|
|
145
139
|
str = value.to_s.strip
|
|
146
140
|
return nil if str.length > max_length
|
|
147
141
|
|
|
148
|
-
# Only allow printable ASCII (whitelist approach)
|
|
149
142
|
unless allow_control_chars
|
|
150
143
|
str = str.gsub(/[^\x20-\x7E]/, "")
|
|
151
144
|
end
|
|
@@ -159,15 +152,16 @@ module OmniauthOpenidFederation
|
|
|
159
152
|
max_length ||= ::OmniauthOpenidFederation::Configuration.config.max_string_length
|
|
160
153
|
raise SecurityError, "URI cannot be nil" if uri_str.nil?
|
|
161
154
|
|
|
162
|
-
|
|
155
|
+
original_str = uri_str.to_s
|
|
156
|
+
sanitized = original_str.gsub(/[^\x20-\x7E]/, "")
|
|
157
|
+
raise SecurityError, "URI contains invalid characters (only printable ASCII allowed)" if sanitized != original_str
|
|
158
|
+
|
|
159
|
+
str = sanitized.strip
|
|
163
160
|
raise SecurityError, "URI cannot be empty" if str.empty?
|
|
164
161
|
raise SecurityError, "URI exceeds maximum length of #{max_length} characters" if str.length > max_length
|
|
165
162
|
|
|
166
|
-
sanitized = str.gsub(/[^\x20-\x7E]/, "")
|
|
167
|
-
raise SecurityError, "URI contains invalid characters (only printable ASCII allowed)" if sanitized != str
|
|
168
|
-
|
|
169
163
|
begin
|
|
170
|
-
parsed = URI.parse(
|
|
164
|
+
parsed = URI.parse(str)
|
|
171
165
|
rescue URI::InvalidURIError => e
|
|
172
166
|
raise SecurityError, "Invalid URI format: #{e.message}"
|
|
173
167
|
end
|
|
@@ -180,7 +174,7 @@ module OmniauthOpenidFederation
|
|
|
180
174
|
raise SecurityError, "URI must be HTTP or HTTPS"
|
|
181
175
|
end
|
|
182
176
|
|
|
183
|
-
raise SecurityError, "URI host cannot be empty" if
|
|
177
|
+
raise SecurityError, "URI host cannot be empty" if StringHelpers.blank?(parsed.host)
|
|
184
178
|
|
|
185
179
|
parsed
|
|
186
180
|
end
|
|
@@ -199,22 +193,16 @@ module OmniauthOpenidFederation
|
|
|
199
193
|
|
|
200
194
|
case acr_values
|
|
201
195
|
when Array
|
|
202
|
-
# Filter out blanks (arrays may already be sanitized)
|
|
203
196
|
values = acr_values.map(&:to_s).map(&:strip).reject { |v| StringHelpers.blank?(v) }
|
|
204
197
|
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
198
|
trimmed = acr_values.strip
|
|
208
199
|
values = trimmed.split(" ").map(&:strip).reject { |v| StringHelpers.blank?(v) }
|
|
209
200
|
else
|
|
210
|
-
# Convert to string, trim and split
|
|
211
201
|
str = acr_values.to_s.strip
|
|
212
202
|
return nil if str.length > max_length
|
|
213
|
-
# Security: Use simple space split (no regexp to avoid ReDoS)
|
|
214
203
|
values = str.split(" ").map(&:strip).reject { |v| StringHelpers.blank?(v) }
|
|
215
204
|
end
|
|
216
205
|
|
|
217
|
-
# Sanitize each value unless already sanitized
|
|
218
206
|
unless skip_sanitization
|
|
219
207
|
values = values.map { |v| sanitize_request_param(v) }.compact
|
|
220
208
|
end
|
|
@@ -243,9 +231,8 @@ module OmniauthOpenidFederation
|
|
|
243
231
|
raise ConfigurationError, "client_id cannot be empty after trimming"
|
|
244
232
|
end
|
|
245
233
|
|
|
246
|
-
# Sanitize using allowed characters (printable ASCII only)
|
|
247
234
|
sanitized = sanitize_request_param(str)
|
|
248
|
-
if
|
|
235
|
+
if StringHelpers.blank?(sanitized)
|
|
249
236
|
raise ConfigurationError, "client_id contains invalid characters"
|
|
250
237
|
end
|
|
251
238
|
|
|
@@ -268,7 +255,6 @@ module OmniauthOpenidFederation
|
|
|
268
255
|
raise ConfigurationError, "redirect_uri cannot be empty after trimming"
|
|
269
256
|
end
|
|
270
257
|
|
|
271
|
-
# Validate as URI (includes allowed characters validation)
|
|
272
258
|
validated = validate_uri_safe!(str, allowed_schemes: ["http", "https"])
|
|
273
259
|
validated.to_s
|
|
274
260
|
end
|
|
@@ -289,7 +275,6 @@ module OmniauthOpenidFederation
|
|
|
289
275
|
return nil
|
|
290
276
|
end
|
|
291
277
|
|
|
292
|
-
# Normalize to array
|
|
293
278
|
scopes = case scope
|
|
294
279
|
when Array
|
|
295
280
|
scope.map(&:to_s).map(&:strip).reject { |s| StringHelpers.blank?(s) }
|
|
@@ -299,14 +284,12 @@ module OmniauthOpenidFederation
|
|
|
299
284
|
scope.to_s.strip.split(" ").map(&:strip).reject { |s| StringHelpers.blank?(s) }
|
|
300
285
|
end
|
|
301
286
|
|
|
302
|
-
# Validate each scope value (allowed: printable ASCII)
|
|
303
287
|
scopes = scopes.map { |s| sanitize_request_param(s) }.compact
|
|
304
288
|
|
|
305
289
|
if scopes.empty?
|
|
306
290
|
raise ConfigurationError, "scope cannot be empty after validation"
|
|
307
291
|
end
|
|
308
292
|
|
|
309
|
-
# Check for "openid" scope if required
|
|
310
293
|
if require_openid && !scopes.include?("openid")
|
|
311
294
|
raise ConfigurationError, "scope MUST include 'openid' per OIDC Core 1.0 spec"
|
|
312
295
|
end
|
|
@@ -336,9 +319,8 @@ module OmniauthOpenidFederation
|
|
|
336
319
|
raise ConfigurationError, "state cannot be empty after trimming"
|
|
337
320
|
end
|
|
338
321
|
|
|
339
|
-
# Sanitize using allowed characters (printable ASCII only)
|
|
340
322
|
sanitized = sanitize_request_param(str)
|
|
341
|
-
if
|
|
323
|
+
if StringHelpers.blank?(sanitized)
|
|
342
324
|
raise ConfigurationError, "state contains invalid characters"
|
|
343
325
|
end
|
|
344
326
|
|
|
@@ -363,10 +345,8 @@ module OmniauthOpenidFederation
|
|
|
363
345
|
return nil
|
|
364
346
|
end
|
|
365
347
|
|
|
366
|
-
# Sanitize using allowed characters (printable ASCII only)
|
|
367
|
-
# OIDC Core 1.0: nonce value is a case-sensitive string
|
|
368
348
|
sanitized = sanitize_request_param(str)
|
|
369
|
-
if
|
|
349
|
+
if StringHelpers.blank?(sanitized)
|
|
370
350
|
if required
|
|
371
351
|
raise ConfigurationError, "nonce contains invalid characters"
|
|
372
352
|
end
|
|
@@ -392,13 +372,11 @@ module OmniauthOpenidFederation
|
|
|
392
372
|
raise ConfigurationError, "response_type cannot be empty after trimming"
|
|
393
373
|
end
|
|
394
374
|
|
|
395
|
-
# Sanitize using allowed characters (printable ASCII only)
|
|
396
375
|
sanitized = sanitize_request_param(str)
|
|
397
|
-
if
|
|
376
|
+
if StringHelpers.blank?(sanitized)
|
|
398
377
|
raise ConfigurationError, "response_type contains invalid characters"
|
|
399
378
|
end
|
|
400
379
|
|
|
401
|
-
# Validate it's a known response type (space-separated list)
|
|
402
380
|
valid_types = ["code", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token"]
|
|
403
381
|
types = sanitized.split(" ").map(&:strip)
|
|
404
382
|
unless types.all? { |t| valid_types.include?(t) || t.match?(/^[a-z_]+$/) }
|
|
@@ -426,10 +404,8 @@ module OmniauthOpenidFederation
|
|
|
426
404
|
raise SecurityError, "Entity identifier cannot be empty after trimming"
|
|
427
405
|
end
|
|
428
406
|
|
|
429
|
-
# Validate as URI (includes allowed characters and length validation)
|
|
430
407
|
validate_uri_safe!(str, max_length: max_length, allowed_schemes: ["http", "https"])
|
|
431
408
|
|
|
432
|
-
# Return trimmed and validated value
|
|
433
409
|
str
|
|
434
410
|
end
|
|
435
411
|
end
|
|
@@ -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"
|
|
@@ -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: #{
|
|
266
|
-
puts " Issued At: #{
|
|
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
|
|
@@ -594,7 +595,7 @@ namespace :openid_federation do
|
|
|
594
595
|
if value
|
|
595
596
|
if [:exp, :iat, :auth_time].include?(key)
|
|
596
597
|
time_value = begin
|
|
597
|
-
|
|
598
|
+
OmniauthOpenidFederation::TimeHelpers.at(value)
|
|
598
599
|
rescue
|
|
599
600
|
value
|
|
600
601
|
end
|
|
@@ -183,6 +183,12 @@ module OmniauthOpenidFederation
|
|
|
183
183
|
def self.blank?: (untyped value) -> bool
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
+
module TimeHelpers
|
|
187
|
+
def self.now: () -> Time
|
|
188
|
+
def self.at: (Integer | Float timestamp) -> Time
|
|
189
|
+
def self.parse: (String time_string) -> Time
|
|
190
|
+
end
|
|
191
|
+
|
|
186
192
|
module Validators
|
|
187
193
|
def self.validate_private_key!: (untyped private_key) -> void
|
|
188
194
|
def self.normalize_hash: (untyped hash) -> Hash[Symbol, untyped]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omniauth_openid_federation
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -197,6 +197,104 @@ dependencies:
|
|
|
197
197
|
- - "~>"
|
|
198
198
|
- !ruby/object:Gem::Version
|
|
199
199
|
version: '1.52'
|
|
200
|
+
- !ruby/object:Gem::Dependency
|
|
201
|
+
name: standard-custom
|
|
202
|
+
requirement: !ruby/object:Gem::Requirement
|
|
203
|
+
requirements:
|
|
204
|
+
- - "~>"
|
|
205
|
+
- !ruby/object:Gem::Version
|
|
206
|
+
version: '1.0'
|
|
207
|
+
type: :development
|
|
208
|
+
prerelease: false
|
|
209
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
210
|
+
requirements:
|
|
211
|
+
- - "~>"
|
|
212
|
+
- !ruby/object:Gem::Version
|
|
213
|
+
version: '1.0'
|
|
214
|
+
- !ruby/object:Gem::Dependency
|
|
215
|
+
name: standard-performance
|
|
216
|
+
requirement: !ruby/object:Gem::Requirement
|
|
217
|
+
requirements:
|
|
218
|
+
- - "~>"
|
|
219
|
+
- !ruby/object:Gem::Version
|
|
220
|
+
version: '1.8'
|
|
221
|
+
type: :development
|
|
222
|
+
prerelease: false
|
|
223
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
224
|
+
requirements:
|
|
225
|
+
- - "~>"
|
|
226
|
+
- !ruby/object:Gem::Version
|
|
227
|
+
version: '1.8'
|
|
228
|
+
- !ruby/object:Gem::Dependency
|
|
229
|
+
name: standard-rails
|
|
230
|
+
requirement: !ruby/object:Gem::Requirement
|
|
231
|
+
requirements:
|
|
232
|
+
- - "~>"
|
|
233
|
+
- !ruby/object:Gem::Version
|
|
234
|
+
version: '1.5'
|
|
235
|
+
type: :development
|
|
236
|
+
prerelease: false
|
|
237
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
238
|
+
requirements:
|
|
239
|
+
- - "~>"
|
|
240
|
+
- !ruby/object:Gem::Version
|
|
241
|
+
version: '1.5'
|
|
242
|
+
- !ruby/object:Gem::Dependency
|
|
243
|
+
name: standard-rspec
|
|
244
|
+
requirement: !ruby/object:Gem::Requirement
|
|
245
|
+
requirements:
|
|
246
|
+
- - "~>"
|
|
247
|
+
- !ruby/object:Gem::Version
|
|
248
|
+
version: '0.3'
|
|
249
|
+
type: :development
|
|
250
|
+
prerelease: false
|
|
251
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
252
|
+
requirements:
|
|
253
|
+
- - "~>"
|
|
254
|
+
- !ruby/object:Gem::Version
|
|
255
|
+
version: '0.3'
|
|
256
|
+
- !ruby/object:Gem::Dependency
|
|
257
|
+
name: rubocop-rails
|
|
258
|
+
requirement: !ruby/object:Gem::Requirement
|
|
259
|
+
requirements:
|
|
260
|
+
- - "~>"
|
|
261
|
+
- !ruby/object:Gem::Version
|
|
262
|
+
version: '2.33'
|
|
263
|
+
type: :development
|
|
264
|
+
prerelease: false
|
|
265
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
266
|
+
requirements:
|
|
267
|
+
- - "~>"
|
|
268
|
+
- !ruby/object:Gem::Version
|
|
269
|
+
version: '2.33'
|
|
270
|
+
- !ruby/object:Gem::Dependency
|
|
271
|
+
name: rubocop-rspec
|
|
272
|
+
requirement: !ruby/object:Gem::Requirement
|
|
273
|
+
requirements:
|
|
274
|
+
- - "~>"
|
|
275
|
+
- !ruby/object:Gem::Version
|
|
276
|
+
version: '3.8'
|
|
277
|
+
type: :development
|
|
278
|
+
prerelease: false
|
|
279
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
280
|
+
requirements:
|
|
281
|
+
- - "~>"
|
|
282
|
+
- !ruby/object:Gem::Version
|
|
283
|
+
version: '3.8'
|
|
284
|
+
- !ruby/object:Gem::Dependency
|
|
285
|
+
name: rubocop-thread_safety
|
|
286
|
+
requirement: !ruby/object:Gem::Requirement
|
|
287
|
+
requirements:
|
|
288
|
+
- - "~>"
|
|
289
|
+
- !ruby/object:Gem::Version
|
|
290
|
+
version: '0.7'
|
|
291
|
+
type: :development
|
|
292
|
+
prerelease: false
|
|
293
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
294
|
+
requirements:
|
|
295
|
+
- - "~>"
|
|
296
|
+
- !ruby/object:Gem::Version
|
|
297
|
+
version: '0.7'
|
|
200
298
|
- !ruby/object:Gem::Dependency
|
|
201
299
|
name: appraisal
|
|
202
300
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -323,6 +421,7 @@ files:
|
|
|
323
421
|
- lib/omniauth_openid_federation/strategy.rb
|
|
324
422
|
- lib/omniauth_openid_federation/string_helpers.rb
|
|
325
423
|
- lib/omniauth_openid_federation/tasks_helper.rb
|
|
424
|
+
- lib/omniauth_openid_federation/time_helpers.rb
|
|
326
425
|
- lib/omniauth_openid_federation/utils.rb
|
|
327
426
|
- lib/omniauth_openid_federation/validators.rb
|
|
328
427
|
- lib/omniauth_openid_federation/version.rb
|