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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -1
- data/README.md +120 -14
- data/app/controllers/omniauth_openid_federation/federation_controller.rb +3 -3
- data/config/polyrun_coverage.yml +15 -0
- data/config/polyrun_spec_quality.yml +29 -0
- data/config/routes.rb +20 -10
- data/examples/README_INTEGRATION_TESTING.md +3 -0
- data/examples/integration_test_flow.rb +10 -8
- data/examples/jobs/federation_cache_refresh_job.rb.example +7 -7
- data/examples/jobs/federation_files_generation_job.rb.example +3 -2
- data/examples/mock_op_server.rb +4 -5
- data/examples/mock_rp_server.rb +3 -3
- data/examples/standalone_multiple_endpoints_example.rb +494 -0
- data/lib/omniauth_openid_federation/access_token.rb +91 -450
- data/lib/omniauth_openid_federation/cache.rb +16 -0
- data/lib/omniauth_openid_federation/cache_adapter.rb +6 -3
- data/lib/omniauth_openid_federation/constants.rb +3 -0
- data/lib/omniauth_openid_federation/entity_statement_reader.rb +39 -14
- data/lib/omniauth_openid_federation/federation/entity_statement.rb +0 -4
- 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 +8 -91
- data/lib/omniauth_openid_federation/federation/signed_jwks.rb +0 -1
- data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +54 -21
- data/lib/omniauth_openid_federation/federation_endpoint.rb +40 -172
- data/lib/omniauth_openid_federation/http_client.rb +145 -32
- data/lib/omniauth_openid_federation/http_errors.rb +14 -0
- data/lib/omniauth_openid_federation/id_token.rb +19 -0
- data/lib/omniauth_openid_federation/jwe.rb +26 -0
- data/lib/omniauth_openid_federation/jwks/decode.rb +0 -13
- data/lib/omniauth_openid_federation/jwks/fetch.rb +16 -39
- data/lib/omniauth_openid_federation/jwks/rotate.rb +45 -20
- data/lib/omniauth_openid_federation/jws.rb +3 -11
- data/lib/omniauth_openid_federation/jwt_response_decoder.rb +290 -0
- data/lib/omniauth_openid_federation/oidc_client.rb +101 -0
- data/lib/omniauth_openid_federation/rack.rb +2 -0
- data/lib/omniauth_openid_federation/rack_endpoint.rb +21 -9
- data/lib/omniauth_openid_federation/rate_limiter.rb +10 -12
- data/lib/omniauth_openid_federation/secure_compare.rb +15 -0
- data/lib/omniauth_openid_federation/strategy/authorization_request.rb +220 -0
- data/lib/omniauth_openid_federation/strategy/callback_handling.rb +135 -0
- data/lib/omniauth_openid_federation/strategy/client_construction.rb +101 -0
- data/lib/omniauth_openid_federation/strategy/client_entity_statement.rb +211 -0
- data/lib/omniauth_openid_federation/strategy/endpoint_resolution.rb +433 -0
- data/lib/omniauth_openid_federation/strategy/failure_handling.rb +75 -0
- data/lib/omniauth_openid_federation/strategy/id_token_decoding.rb +268 -0
- data/lib/omniauth_openid_federation/strategy/jwks_resolution.rb +268 -0
- data/lib/omniauth_openid_federation/strategy/provider_entity_statement.rb +134 -0
- data/lib/omniauth_openid_federation/strategy/userinfo_decoding.rb +61 -0
- data/lib/omniauth_openid_federation/strategy.rb +27 -1910
- data/lib/omniauth_openid_federation/tasks/authentication_flow_tester.rb +237 -0
- data/lib/omniauth_openid_federation/tasks/callback_processor.rb +235 -0
- data/lib/omniauth_openid_federation/tasks/client_keys_preparer.rb +96 -0
- data/lib/omniauth_openid_federation/tasks/local_endpoint_tester.rb +189 -0
- data/lib/omniauth_openid_federation/tasks/path_resolver.rb +20 -0
- data/lib/omniauth_openid_federation/tasks_helper.rb +28 -790
- data/lib/omniauth_openid_federation/time_helpers.rb +67 -0
- data/lib/omniauth_openid_federation/user_info.rb +15 -0
- data/lib/omniauth_openid_federation/utils.rb +14 -9
- data/lib/omniauth_openid_federation/validators.rb +55 -44
- data/lib/omniauth_openid_federation/version.rb +1 -1
- data/lib/omniauth_openid_federation.rb +10 -3
- data/lib/tasks/omniauth_openid_federation.rake +5 -5
- data/sig/omniauth_openid_federation.rbs +8 -1
- data/sig/strategy.rbs +6 -6
- metadata +252 -43
|
@@ -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
|
|
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
|
|
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
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
require "net/http"
|
|
2
1
|
require "digest"
|
|
3
2
|
require "jwt"
|
|
4
3
|
require "base64"
|
|
@@ -94,9 +93,6 @@ module OmniauthOpenidFederation
|
|
|
94
93
|
# @raise [FetchError] If fetching fails
|
|
95
94
|
# @raise [ValidationError] If validation fails
|
|
96
95
|
def self.fetch!(url, fingerprint: nil, previous_statement: nil, timeout: 10)
|
|
97
|
-
# Use HttpClient for retry logic and configurable SSL verification
|
|
98
|
-
# Note: HttpClient uses HTTP gem, but entity statements might need Net::HTTP
|
|
99
|
-
# For now, we'll use a simple HTTP.get approach with HttpClient's retry logic
|
|
100
96
|
begin
|
|
101
97
|
# Convert URL to URI for HttpClient
|
|
102
98
|
response = HttpClient.get(url, timeout: timeout)
|
|
@@ -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
|
|
106
|
-
raise ConfigurationError, "Subject is required" if
|
|
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
|
|
109
|
-
raise ConfigurationError, "Metadata is required" if
|
|
110
|
-
raise ConfigurationError, "JWKS must contain at least one key" if
|
|
111
|
-
raise ConfigurationError, "Key ID (kid) is required" if
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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)
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require_relative "../logger"
|
|
5
5
|
require_relative "../errors"
|
|
6
6
|
require_relative "../configuration"
|
|
7
|
+
require_relative "../string_helpers"
|
|
7
8
|
|
|
8
9
|
# Entity Statement Validator for OpenID Federation 1.0
|
|
9
10
|
# @see https://openid.net/specs/openid-federation-1_0.html#section-3.2.1 Section 3.2.1: Entity Statement Validation
|
|
@@ -53,70 +54,27 @@ module OmniauthOpenidFederation
|
|
|
53
54
|
# @return [Hash] Validated entity statement with header and claims
|
|
54
55
|
# @raise [ValidationError] If validation fails at any step
|
|
55
56
|
def validate!
|
|
56
|
-
# Step 1: Entity Statement MUST be a signed JWT
|
|
57
57
|
validate_jwt_format
|
|
58
|
-
|
|
59
|
-
# Step 2: Validate typ header (MUST be "entity-statement+jwt")
|
|
60
58
|
validate_typ_header
|
|
61
|
-
|
|
62
|
-
# Step 3: Validate alg header (MUST be present and not "none")
|
|
63
59
|
validate_alg_header
|
|
64
|
-
|
|
65
|
-
# Step 4: Validate sub claim matches Entity Identifier
|
|
66
60
|
validate_sub_claim
|
|
67
|
-
|
|
68
|
-
# Step 5: Validate iss claim is valid Entity Identifier
|
|
69
61
|
validate_iss_claim
|
|
70
|
-
|
|
71
|
-
# Step 6: Determine Entity Configuration vs Subordinate Statement
|
|
72
62
|
determine_statement_type
|
|
73
|
-
|
|
74
|
-
# Step 7: Validate authority_hints for Subordinate Statements
|
|
75
63
|
validate_authority_hints if @is_subordinate_statement
|
|
76
|
-
|
|
77
|
-
# Step 8: Validate iat claim (issued at time)
|
|
78
64
|
validate_iat_claim
|
|
79
|
-
|
|
80
|
-
# Step 9: Validate exp claim (expiration time)
|
|
81
65
|
validate_exp_claim
|
|
82
|
-
|
|
83
|
-
# Step 10: Validate jwks claim (MUST be present and valid)
|
|
84
66
|
validate_jwks_claim
|
|
85
|
-
|
|
86
|
-
# Step 11: Validate kid header (MUST be non-zero length string)
|
|
87
67
|
validate_kid_header
|
|
88
|
-
|
|
89
|
-
# Step 12: Validate kid matches key in issuer's JWKS
|
|
90
68
|
validate_kid_matching
|
|
91
|
-
|
|
92
|
-
# Step 13: Validate signature (if issuer configuration provided)
|
|
93
69
|
validate_signature if @issuer_entity_configuration || @is_entity_configuration
|
|
94
|
-
|
|
95
|
-
# Step 14: Validate crit claim (if present)
|
|
96
70
|
validate_crit_claim
|
|
97
|
-
|
|
98
|
-
# Step 15: Validate authority_hints syntax (if present)
|
|
99
71
|
validate_authority_hints_syntax if @header && @payload && @is_entity_configuration
|
|
100
|
-
|
|
101
|
-
# Step 16: Validate metadata syntax (if present)
|
|
102
72
|
validate_metadata_syntax
|
|
103
|
-
|
|
104
|
-
# Step 17: Validate metadata_policy (if present, MUST be Subordinate Statement)
|
|
105
73
|
validate_metadata_policy_presence
|
|
106
|
-
|
|
107
|
-
# Step 18: Validate metadata_policy_crit (if present, MUST be Subordinate Statement)
|
|
108
74
|
validate_metadata_policy_crit_presence
|
|
109
|
-
|
|
110
|
-
# Step 19: Validate constraints (if present, MUST be Subordinate Statement)
|
|
111
75
|
validate_constraints_presence
|
|
112
|
-
|
|
113
|
-
# Step 20: Validate trust_marks (if present, MUST be Entity Configuration)
|
|
114
76
|
validate_trust_marks_presence
|
|
115
|
-
|
|
116
|
-
# Step 21: Validate trust_mark_issuers (if present, MUST be Entity Configuration)
|
|
117
77
|
validate_trust_mark_issuers_presence
|
|
118
|
-
|
|
119
|
-
# Step 22: Validate trust_mark_owners (if present, MUST be Entity Configuration)
|
|
120
78
|
validate_trust_mark_owners_presence
|
|
121
79
|
|
|
122
80
|
{
|
|
@@ -129,14 +87,12 @@ module OmniauthOpenidFederation
|
|
|
129
87
|
|
|
130
88
|
private
|
|
131
89
|
|
|
132
|
-
# Step 1: Entity Statement MUST be a signed JWT
|
|
133
90
|
def validate_jwt_format
|
|
134
91
|
jwt_parts = @jwt_string.split(".")
|
|
135
92
|
if jwt_parts.length != JWT_PARTS_COUNT
|
|
136
93
|
raise ValidationError, "Invalid JWT format: expected #{JWT_PARTS_COUNT} parts (header.payload.signature), got #{jwt_parts.length}"
|
|
137
94
|
end
|
|
138
95
|
|
|
139
|
-
# Decode header and payload
|
|
140
96
|
begin
|
|
141
97
|
@header = JSON.parse(Base64.urlsafe_decode64(jwt_parts[0]))
|
|
142
98
|
@payload = JSON.parse(Base64.urlsafe_decode64(jwt_parts[1]))
|
|
@@ -147,7 +103,6 @@ module OmniauthOpenidFederation
|
|
|
147
103
|
end
|
|
148
104
|
end
|
|
149
105
|
|
|
150
|
-
# Step 2: Entity Statement MUST have typ header with value "entity-statement+jwt"
|
|
151
106
|
def validate_typ_header
|
|
152
107
|
typ = @header["typ"] || @header[:typ]
|
|
153
108
|
unless typ == REQUIRED_TYP
|
|
@@ -155,10 +110,9 @@ module OmniauthOpenidFederation
|
|
|
155
110
|
end
|
|
156
111
|
end
|
|
157
112
|
|
|
158
|
-
# Step 3: Entity Statement MUST have alg header that is present and not "none"
|
|
159
113
|
def validate_alg_header
|
|
160
114
|
alg = @header["alg"] || @header[:alg]
|
|
161
|
-
if
|
|
115
|
+
if StringHelpers.blank?(alg)
|
|
162
116
|
raise ValidationError, "Entity statement MUST have an alg (algorithm) header parameter"
|
|
163
117
|
end
|
|
164
118
|
if alg == "none"
|
|
@@ -170,33 +124,28 @@ module OmniauthOpenidFederation
|
|
|
170
124
|
end
|
|
171
125
|
end
|
|
172
126
|
|
|
173
|
-
# Step 4: Entity Identifier MUST match sub claim
|
|
174
127
|
# Note: This is a structural check. The actual Entity Identifier validation
|
|
175
128
|
# would require knowing the expected Entity Identifier, which is context-dependent.
|
|
176
129
|
def validate_sub_claim
|
|
177
130
|
sub = @payload["sub"]
|
|
178
|
-
if
|
|
131
|
+
if StringHelpers.blank?(sub)
|
|
179
132
|
raise ValidationError, "Entity statement MUST have a sub (subject) claim with a valid Entity Identifier"
|
|
180
133
|
end
|
|
181
|
-
# Basic Entity Identifier format validation (should be a URI)
|
|
182
134
|
unless sub.is_a?(String) && sub.start_with?("http://", "https://")
|
|
183
135
|
raise ValidationError, "Entity statement sub claim MUST be a valid Entity Identifier (URI)"
|
|
184
136
|
end
|
|
185
137
|
end
|
|
186
138
|
|
|
187
|
-
# Step 5: Entity Statement MUST have iss claim with valid Entity Identifier
|
|
188
139
|
def validate_iss_claim
|
|
189
140
|
iss = @payload["iss"]
|
|
190
|
-
if
|
|
141
|
+
if StringHelpers.blank?(iss)
|
|
191
142
|
raise ValidationError, "Entity statement MUST have an iss (issuer) claim with a valid Entity Identifier"
|
|
192
143
|
end
|
|
193
|
-
# Basic Entity Identifier format validation (should be a URI)
|
|
194
144
|
unless iss.is_a?(String) && iss.start_with?("http://", "https://")
|
|
195
145
|
raise ValidationError, "Entity statement iss claim MUST be a valid Entity Identifier (URI)"
|
|
196
146
|
end
|
|
197
147
|
end
|
|
198
148
|
|
|
199
|
-
# Step 6: Determine Entity Configuration vs Subordinate Statement
|
|
200
149
|
def determine_statement_type
|
|
201
150
|
iss = @payload["iss"]
|
|
202
151
|
sub = @payload["sub"]
|
|
@@ -213,7 +162,6 @@ module OmniauthOpenidFederation
|
|
|
213
162
|
return
|
|
214
163
|
end
|
|
215
164
|
|
|
216
|
-
# Extract authority_hints from issuer's Entity Configuration
|
|
217
165
|
issuer_config = if @issuer_entity_configuration.is_a?(Hash)
|
|
218
166
|
@issuer_entity_configuration
|
|
219
167
|
elsif @issuer_entity_configuration.respond_to?(:parse)
|
|
@@ -231,7 +179,6 @@ module OmniauthOpenidFederation
|
|
|
231
179
|
end
|
|
232
180
|
end
|
|
233
181
|
|
|
234
|
-
# Step 8: Validate iat claim (issued at time)
|
|
235
182
|
def validate_iat_claim
|
|
236
183
|
iat = @payload["iat"]
|
|
237
184
|
if iat.nil?
|
|
@@ -243,13 +190,11 @@ module OmniauthOpenidFederation
|
|
|
243
190
|
end
|
|
244
191
|
|
|
245
192
|
current_time = Time.now.to_i
|
|
246
|
-
# Allow clock skew: iat can be slightly in the future
|
|
247
193
|
if iat > (current_time + @clock_skew_tolerance)
|
|
248
194
|
raise ValidationError, "Entity statement iat (issued at) claim is too far in the future. Current time: #{current_time}, iat: #{iat}, tolerance: #{@clock_skew_tolerance}s"
|
|
249
195
|
end
|
|
250
196
|
end
|
|
251
197
|
|
|
252
|
-
# Step 9: Validate exp claim (expiration time)
|
|
253
198
|
def validate_exp_claim
|
|
254
199
|
exp = @payload["exp"]
|
|
255
200
|
if exp.nil?
|
|
@@ -261,19 +206,16 @@ module OmniauthOpenidFederation
|
|
|
261
206
|
end
|
|
262
207
|
|
|
263
208
|
current_time = Time.now.to_i
|
|
264
|
-
# Allow clock skew: exp can be slightly in the past
|
|
265
209
|
if exp < (current_time - @clock_skew_tolerance)
|
|
266
210
|
raise ValidationError, "Entity statement has expired. Current time: #{current_time}, exp: #{exp}, tolerance: #{@clock_skew_tolerance}s"
|
|
267
211
|
end
|
|
268
212
|
end
|
|
269
213
|
|
|
270
|
-
# Step 10: Validate jwks claim (MUST be present and valid)
|
|
271
214
|
def validate_jwks_claim
|
|
272
215
|
jwks = @payload["jwks"]
|
|
273
216
|
if jwks.nil?
|
|
274
217
|
# jwks is OPTIONAL only for Entity Statement returned from OP during Explicit Registration
|
|
275
218
|
# For all other cases, it is REQUIRED
|
|
276
|
-
# We'll be strict and require it unless we have context that this is an Explicit Registration response
|
|
277
219
|
raise ValidationError, "Entity statement MUST have a jwks (JWK Set) claim"
|
|
278
220
|
end
|
|
279
221
|
|
|
@@ -286,14 +228,12 @@ module OmniauthOpenidFederation
|
|
|
286
228
|
raise ValidationError, "Entity statement jwks claim MUST contain a 'keys' array"
|
|
287
229
|
end
|
|
288
230
|
|
|
289
|
-
# Validate that each key has a unique kid
|
|
290
231
|
kids = keys.map { |key| key["kid"] || key[:kid] }.compact
|
|
291
232
|
if kids.length != kids.uniq.length
|
|
292
233
|
raise ValidationError, "Entity statement jwks keys MUST have unique kid (Key ID) values"
|
|
293
234
|
end
|
|
294
235
|
end
|
|
295
236
|
|
|
296
|
-
# Step 11: Validate kid header (MUST be non-zero length string)
|
|
297
237
|
def validate_kid_header
|
|
298
238
|
kid = @header["kid"] || @header[:kid]
|
|
299
239
|
if kid.nil?
|
|
@@ -307,17 +247,13 @@ module OmniauthOpenidFederation
|
|
|
307
247
|
end
|
|
308
248
|
end
|
|
309
249
|
|
|
310
|
-
# Step 12: Validate kid matches key in issuer's JWKS
|
|
311
250
|
def validate_kid_matching
|
|
312
251
|
kid = @header["kid"] || @header[:kid]
|
|
313
252
|
jwks = @payload["jwks"] || {}
|
|
314
253
|
|
|
315
|
-
# Get issuer's JWKS
|
|
316
254
|
issuer_jwks = if @is_entity_configuration
|
|
317
|
-
# For Entity Configuration, use its own JWKS
|
|
318
255
|
jwks
|
|
319
256
|
elsif @issuer_entity_configuration
|
|
320
|
-
# For Subordinate Statement, use issuer's Entity Configuration JWKS
|
|
321
257
|
issuer_config = if @issuer_entity_configuration.is_a?(Hash)
|
|
322
258
|
@issuer_entity_configuration
|
|
323
259
|
elsif @issuer_entity_configuration.respond_to?(:parse)
|
|
@@ -328,7 +264,6 @@ module OmniauthOpenidFederation
|
|
|
328
264
|
|
|
329
265
|
issuer_config[:jwks] || issuer_config["jwks"] || issuer_config[:claims]&.fetch("jwks", {}) || issuer_config["claims"]&.fetch("jwks", {})
|
|
330
266
|
else
|
|
331
|
-
# Cannot validate kid matching without issuer configuration
|
|
332
267
|
OmniauthOpenidFederation::Logger.warn("[EntityStatementValidator] Cannot validate kid matching: issuer entity configuration not provided")
|
|
333
268
|
return
|
|
334
269
|
end
|
|
@@ -341,12 +276,9 @@ module OmniauthOpenidFederation
|
|
|
341
276
|
end
|
|
342
277
|
end
|
|
343
278
|
|
|
344
|
-
#
|
|
279
|
+
# Signature validation is typically done separately using JWT.decode
|
|
280
|
+
# This step validates that we have the necessary information to validate the signature
|
|
345
281
|
def validate_signature
|
|
346
|
-
# Signature validation is typically done separately using JWT.decode
|
|
347
|
-
# This step is a placeholder - actual signature validation should be done
|
|
348
|
-
# by the caller using the issuer's public key
|
|
349
|
-
# We validate that we have the necessary information to validate the signature
|
|
350
282
|
kid = @header["kid"] || @header[:kid]
|
|
351
283
|
jwks = @payload["jwks"] || {}
|
|
352
284
|
|
|
@@ -372,12 +304,8 @@ module OmniauthOpenidFederation
|
|
|
372
304
|
unless matching_key
|
|
373
305
|
raise ValidationError, "Cannot validate signature: signing key with kid '#{kid}' not found in issuer's JWKS"
|
|
374
306
|
end
|
|
375
|
-
|
|
376
|
-
# Note: Actual cryptographic signature verification should be done by the caller
|
|
377
|
-
# using JWT.decode with the matching key
|
|
378
307
|
end
|
|
379
308
|
|
|
380
|
-
# Step 14: Validate crit claim (if present)
|
|
381
309
|
def validate_crit_claim
|
|
382
310
|
crit = @payload["crit"] || @payload[:crit]
|
|
383
311
|
return unless crit
|
|
@@ -391,14 +319,11 @@ module OmniauthOpenidFederation
|
|
|
391
319
|
unknown_claims = crit - standard_claims
|
|
392
320
|
|
|
393
321
|
if unknown_claims.any?
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
# Future enhancement: Add strict_mode option to reject unknown crit claims
|
|
397
|
-
OmniauthOpenidFederation::Logger.warn("[EntityStatementValidator] Entity statement contains crit claim with unknown claims: #{unknown_claims.join(", ")}. These claims MUST be understood and processed.")
|
|
322
|
+
raise ValidationError,
|
|
323
|
+
"Entity statement crit claim contains unknown claims that MUST be understood: #{unknown_claims.join(", ")}"
|
|
398
324
|
end
|
|
399
325
|
end
|
|
400
326
|
|
|
401
|
-
# Step 15: Validate authority_hints syntax (if present)
|
|
402
327
|
def validate_authority_hints_syntax
|
|
403
328
|
authority_hints = @payload["authority_hints"] || @payload[:authority_hints]
|
|
404
329
|
return unless authority_hints
|
|
@@ -418,7 +343,6 @@ module OmniauthOpenidFederation
|
|
|
418
343
|
end
|
|
419
344
|
end
|
|
420
345
|
|
|
421
|
-
# Step 16: Validate metadata syntax (if present)
|
|
422
346
|
def validate_metadata_syntax
|
|
423
347
|
metadata = @payload["metadata"] || @payload[:metadata]
|
|
424
348
|
return unless metadata
|
|
@@ -427,7 +351,6 @@ module OmniauthOpenidFederation
|
|
|
427
351
|
raise ValidationError, "Entity statement metadata claim MUST be a JSON object"
|
|
428
352
|
end
|
|
429
353
|
|
|
430
|
-
# Validate that metadata values are not null
|
|
431
354
|
metadata.each do |entity_type, entity_metadata|
|
|
432
355
|
if entity_metadata.nil?
|
|
433
356
|
raise ValidationError, "Entity statement metadata claim MUST NOT use null as metadata values"
|
|
@@ -438,7 +361,6 @@ module OmniauthOpenidFederation
|
|
|
438
361
|
end
|
|
439
362
|
end
|
|
440
363
|
|
|
441
|
-
# Step 17: Validate metadata_policy presence (MUST be Subordinate Statement)
|
|
442
364
|
def validate_metadata_policy_presence
|
|
443
365
|
metadata_policy = @payload["metadata_policy"] || @payload[:metadata_policy]
|
|
444
366
|
return unless metadata_policy
|
|
@@ -448,7 +370,6 @@ module OmniauthOpenidFederation
|
|
|
448
370
|
end
|
|
449
371
|
end
|
|
450
372
|
|
|
451
|
-
# Step 18: Validate metadata_policy_crit presence (MUST be Subordinate Statement)
|
|
452
373
|
def validate_metadata_policy_crit_presence
|
|
453
374
|
metadata_policy_crit = @payload["metadata_policy_crit"] || @payload[:metadata_policy_crit]
|
|
454
375
|
return unless metadata_policy_crit
|
|
@@ -458,7 +379,6 @@ module OmniauthOpenidFederation
|
|
|
458
379
|
end
|
|
459
380
|
end
|
|
460
381
|
|
|
461
|
-
# Step 19: Validate constraints presence (MUST be Subordinate Statement)
|
|
462
382
|
def validate_constraints_presence
|
|
463
383
|
constraints = @payload["constraints"] || @payload[:constraints]
|
|
464
384
|
return unless constraints
|
|
@@ -468,7 +388,6 @@ module OmniauthOpenidFederation
|
|
|
468
388
|
end
|
|
469
389
|
end
|
|
470
390
|
|
|
471
|
-
# Step 20: Validate trust_marks presence (MUST be Entity Configuration)
|
|
472
391
|
def validate_trust_marks_presence
|
|
473
392
|
trust_marks = @payload["trust_marks"] || @payload[:trust_marks]
|
|
474
393
|
return unless trust_marks
|
|
@@ -478,7 +397,6 @@ module OmniauthOpenidFederation
|
|
|
478
397
|
end
|
|
479
398
|
end
|
|
480
399
|
|
|
481
|
-
# Step 21: Validate trust_mark_issuers presence (MUST be Entity Configuration)
|
|
482
400
|
def validate_trust_mark_issuers_presence
|
|
483
401
|
trust_mark_issuers = @payload["trust_mark_issuers"] || @payload[:trust_mark_issuers]
|
|
484
402
|
return unless trust_mark_issuers
|
|
@@ -488,7 +406,6 @@ module OmniauthOpenidFederation
|
|
|
488
406
|
end
|
|
489
407
|
end
|
|
490
408
|
|
|
491
|
-
# Step 22: Validate trust_mark_owners presence (MUST be Entity Configuration)
|
|
492
409
|
def validate_trust_mark_owners_presence
|
|
493
410
|
trust_mark_owners = @payload["trust_mark_owners"] || @payload[:trust_mark_owners]
|
|
494
411
|
return unless trust_mark_owners
|