omniauth_openid_federation 1.2.2 → 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 +20 -1
- data/README.md +210 -708
- data/app/controllers/omniauth_openid_federation/federation_controller.rb +14 -1
- data/config/routes.rb +20 -10
- data/examples/config/initializers/devise.rb.example +44 -55
- data/examples/config/initializers/federation_endpoint.rb.example +2 -2
- data/examples/config/open_id_connect_config.rb.example +12 -15
- data/examples/config/routes.rb.example +9 -5
- 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/configuration.rb +8 -0
- data/lib/omniauth_openid_federation/constants.rb +5 -0
- 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 -193
- data/lib/omniauth_openid_federation/jwks/decode.rb +0 -15
- data/lib/omniauth_openid_federation/jwks/rotate.rb +45 -20
- data/lib/omniauth_openid_federation/jws.rb +23 -20
- data/lib/omniauth_openid_federation/rack_endpoint.rb +30 -5
- data/lib/omniauth_openid_federation/strategy.rb +143 -194
- data/lib/omniauth_openid_federation/tasks_helper.rb +501 -2
- 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 +294 -8
- data/lib/omniauth_openid_federation/version.rb +1 -1
- data/lib/omniauth_openid_federation.rb +1 -0
- data/lib/tasks/omniauth_openid_federation.rake +301 -2
- data/sig/federation.rbs +0 -8
- data/sig/jwks.rbs +0 -6
- data/sig/omniauth_openid_federation.rbs +6 -1
- data/sig/strategy.rbs +0 -2
- metadata +100 -1
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
@@ -3,6 +3,7 @@ require "jwe"
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "base64"
|
|
5
5
|
require_relative "string_helpers"
|
|
6
|
+
require_relative "time_helpers"
|
|
6
7
|
require_relative "logger"
|
|
7
8
|
require_relative "errors"
|
|
8
9
|
require_relative "validators"
|
|
@@ -63,10 +64,6 @@ module OmniauthOpenidFederation
|
|
|
63
64
|
STATE_BYTES = 16 # Number of hex bytes for state parameter
|
|
64
65
|
|
|
65
66
|
attr_accessor :private_key, :state, :nonce
|
|
66
|
-
# Provider-specific extension parameters (outside JWT)
|
|
67
|
-
# Some providers may require additional parameters that are not part of the JWT
|
|
68
|
-
# @deprecated Use request_object_params option in strategy instead (adds params to the JWT request object)
|
|
69
|
-
attr_accessor :ftn_spname
|
|
70
67
|
|
|
71
68
|
# Initialize JWT request object builder
|
|
72
69
|
#
|
|
@@ -114,21 +111,27 @@ module OmniauthOpenidFederation
|
|
|
114
111
|
key_source: :local,
|
|
115
112
|
client_entity_statement: nil
|
|
116
113
|
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@
|
|
121
|
-
@
|
|
122
|
-
@
|
|
123
|
-
@
|
|
124
|
-
@
|
|
125
|
-
@
|
|
126
|
-
@
|
|
127
|
-
@
|
|
128
|
-
@
|
|
129
|
-
|
|
130
|
-
@
|
|
131
|
-
@
|
|
114
|
+
# Security: User input parameters are validated and sanitized before reaching here
|
|
115
|
+
# Configuration parameters are trusted and only trimmed for consistency
|
|
116
|
+
# Store trimmed values to ensure consistency
|
|
117
|
+
@client_id = client_id.to_s.strip
|
|
118
|
+
@redirect_uri = redirect_uri.to_s.strip
|
|
119
|
+
@scope = scope.to_s.strip
|
|
120
|
+
@issuer = issuer&.to_s&.strip
|
|
121
|
+
@audience = audience&.to_s&.strip
|
|
122
|
+
@state = state&.to_s&.strip || SecureRandom.hex(STATE_BYTES)
|
|
123
|
+
@nonce = nonce&.to_s&.strip
|
|
124
|
+
@response_type = response_type.to_s.strip
|
|
125
|
+
@response_mode = response_mode&.to_s&.strip
|
|
126
|
+
# User input parameters (already sanitized)
|
|
127
|
+
@login_hint = login_hint&.to_s&.strip
|
|
128
|
+
@ui_locales = ui_locales&.to_s&.strip
|
|
129
|
+
@claims_locales = claims_locales&.to_s&.strip
|
|
130
|
+
# Configuration parameters (trusted, only trimmed)
|
|
131
|
+
@prompt = prompt&.to_s&.strip
|
|
132
|
+
@hd = hd&.to_s&.strip
|
|
133
|
+
# User input parameter (already sanitized)
|
|
134
|
+
@acr_values = acr_values&.to_s&.strip
|
|
132
135
|
@extra_params = extra_params
|
|
133
136
|
@jwks = jwks
|
|
134
137
|
@entity_statement_path = entity_statement_path
|
|
@@ -220,7 +223,7 @@ module OmniauthOpenidFederation
|
|
|
220
223
|
response_type: @response_type,
|
|
221
224
|
scope: @scope,
|
|
222
225
|
state: state,
|
|
223
|
-
exp: (
|
|
226
|
+
exp: (TimeHelpers.now + (defined?(ActiveSupport) ? REQUEST_OBJECT_EXPIRATION_MINUTES.minutes : REQUEST_OBJECT_EXPIRATION_SECONDS)).to_i,
|
|
224
227
|
jti: SecureRandom.uuid # JWT ID to prevent replay
|
|
225
228
|
}
|
|
226
229
|
|
|
@@ -25,9 +25,11 @@
|
|
|
25
25
|
require "rack"
|
|
26
26
|
require "json"
|
|
27
27
|
require "digest"
|
|
28
|
+
require "uri"
|
|
28
29
|
require_relative "cache_adapter"
|
|
29
30
|
require_relative "federation_endpoint"
|
|
30
31
|
require_relative "logger"
|
|
32
|
+
require_relative "constants"
|
|
31
33
|
|
|
32
34
|
module OmniauthOpenidFederation
|
|
33
35
|
class RackEndpoint
|
|
@@ -114,20 +116,31 @@ module OmniauthOpenidFederation
|
|
|
114
116
|
subject_entity_id = request.params["sub"]
|
|
115
117
|
|
|
116
118
|
unless subject_entity_id
|
|
117
|
-
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
|
+
end
|
|
121
|
+
|
|
122
|
+
# Security: Validate entity identifier per OpenID Federation 1.0 spec
|
|
123
|
+
# Entity identifiers must be valid HTTP/HTTPS URIs
|
|
124
|
+
begin
|
|
125
|
+
# Validate and get trimmed value
|
|
126
|
+
subject_entity_id = OmniauthOpenidFederation::Validators.validate_entity_identifier!(subject_entity_id)
|
|
127
|
+
rescue SecurityError => e
|
|
128
|
+
return error_response(400, {error: "invalid_request", error_description: "Invalid subject entity ID: #{e.message}"})
|
|
129
|
+
rescue => e
|
|
130
|
+
return error_response(400, {error: "invalid_request", error_description: "Subject entity ID validation failed: #{e.message}"})
|
|
118
131
|
end
|
|
119
132
|
|
|
120
133
|
# Validate that subject is not the issuer (invalid request per spec)
|
|
121
134
|
config = OmniauthOpenidFederation::FederationEndpoint.configuration
|
|
122
135
|
if subject_entity_id == config.issuer
|
|
123
|
-
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"})
|
|
124
137
|
end
|
|
125
138
|
|
|
126
139
|
# Get Subordinate Statement
|
|
127
140
|
subordinate_statement = OmniauthOpenidFederation::FederationEndpoint.get_subordinate_statement(subject_entity_id)
|
|
128
141
|
|
|
129
142
|
unless subordinate_statement
|
|
130
|
-
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}"})
|
|
131
144
|
end
|
|
132
145
|
|
|
133
146
|
headers = {
|
|
@@ -175,11 +188,23 @@ module OmniauthOpenidFederation
|
|
|
175
188
|
# Return error response
|
|
176
189
|
#
|
|
177
190
|
# @param status [Integer] HTTP status code
|
|
178
|
-
# @param message [String] Error message
|
|
191
|
+
# @param message [String, Hash] Error message (string) or error hash (will be converted to JSON)
|
|
179
192
|
# @return [Array] Rack response
|
|
180
193
|
def error_response(status, message)
|
|
181
194
|
content_type = (status == 503) ? "text/plain" : "application/json"
|
|
182
|
-
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
|
|
183
208
|
|
|
184
209
|
[status, {"Content-Type" => content_type}, [body]]
|
|
185
210
|
end
|