Module: OmniauthOpenidFederation::Validators

Defined in:
lib/omniauth_openid_federation/validators.rb

Class Method Summary collapse

Class Method Details

.normalize_acr_values(acr_values, max_length: nil, skip_sanitization: false) ⇒ String?

Validate and normalize acr_values parameter per OIDC Core 1.0 spec acr_values is a space-separated string of ACR values Security: Uses allowed characters approach - only allows printable ASCII characters

Parameters:

  • acr_values (String, Array, nil)

    ACR values in any format

  • max_length (Integer) (defaults to: nil)

    Maximum total length (default: Configuration.config.max_string_length)

  • skip_sanitization (Boolean) (defaults to: false)

    Skip sanitization if values are already sanitized (default: false)

Returns:

  • (String, nil)

    Normalized space-separated string or nil



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/omniauth_openid_federation/validators.rb', line 190

def self.normalize_acr_values(acr_values, max_length: nil, skip_sanitization: false)
  max_length ||= ::OmniauthOpenidFederation::Configuration.config.max_string_length
  return nil if StringHelpers.blank?(acr_values)

  case acr_values
  when Array
    values = acr_values.map(&:to_s).map(&:strip).reject { |v| StringHelpers.blank?(v) }
  when String
    trimmed = acr_values.strip
    values = trimmed.split(" ").map(&:strip).reject { |v| StringHelpers.blank?(v) }
  else
    str = acr_values.to_s.strip
    return nil if str.length > max_length
    values = str.split(" ").map(&:strip).reject { |v| StringHelpers.blank?(v) }
  end

  unless skip_sanitization
    values = values.map { |v| sanitize_request_param(v) }.compact
  end

  return nil if values.empty?

  result = values.join(" ")
  return nil if result.length > max_length

  result
end

.normalize_hash(hash) ⇒ Hash

Normalize hash keys to symbols

Parameters:

  • hash (Hash)

    The hash to normalize

Returns:

  • (Hash)

    Hash with symbol keys



123
124
125
126
127
128
129
130
# File 'lib/omniauth_openid_federation/validators.rb', line 123

def self.normalize_hash(hash)
  return {} if hash.nil?

  hash.each_with_object({}) do |(k, v), result|
    key = k.is_a?(String) ? k.to_sym : k
    result[key] = v
  end
end

.sanitize_request_param(value, max_length: nil, allow_control_chars: false) ⇒ Object

Validate and sanitize user input from HTTP requests only (not config values) Prevents URI exploitation, ReDoS, string overflow, and control character attacks Default max_length uses Configuration.config.max_string_length (8KB default) - large enough for legitimate use, prevents DoS attacks



135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/omniauth_openid_federation/validators.rb', line 135

def self.sanitize_request_param(value, max_length: nil, allow_control_chars: false)
  max_length ||= ::OmniauthOpenidFederation::Configuration.config.max_string_length
  return nil if value.nil?

  str = value.to_s.strip
  return nil if str.length > max_length

  unless allow_control_chars
    str = str.gsub(/[^\x20-\x7E]/, "")
  end

  str.empty? ? nil : str
end

.validate_client_id!(client_id) ⇒ String

Validate and sanitize client_id per OIDC Core 1.0 spec client_id is REQUIRED and must be a valid string

Parameters:

  • client_id (String, nil)

    Client identifier

Returns:

  • (String)

    Sanitized client_id

Raises:



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/omniauth_openid_federation/validators.rb', line 224

def self.validate_client_id!(client_id)
  if StringHelpers.blank?(client_id)
    raise ConfigurationError, "client_id is REQUIRED per OIDC Core 1.0 spec"
  end

  str = client_id.to_s.strip
  if str.empty?
    raise ConfigurationError, "client_id cannot be empty after trimming"
  end

  sanitized = sanitize_request_param(str)
  if StringHelpers.blank?(sanitized)
    raise ConfigurationError, "client_id contains invalid characters"
  end

  sanitized
end

.validate_client_options!(client_options) ⇒ Object

Validate client options hash (for configuration validation only) Note: This validates configuration structure, not security (config is trusted)

Parameters:

  • client_options (Hash)

    The client options to validate

Raises:



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/omniauth_openid_federation/validators.rb', line 82

def self.validate_client_options!(client_options)
  client_options ||= {}

  normalized = normalize_hash(client_options)

  if StringHelpers.blank?(normalized[:identifier])
    raise ConfigurationError, "Client identifier is required"
  end

  if StringHelpers.blank?(normalized[:redirect_uri])
    raise ConfigurationError, "Redirect URI is required"
  end

  begin
    parsed = URI.parse(normalized[:redirect_uri].to_s)
    unless parsed.is_a?(URI::HTTP) || parsed.is_a?(URI::HTTPS)
      raise ConfigurationError, "Redirect URI must be HTTP or HTTPS: #{normalized[:redirect_uri]}"
    end
  rescue URI::InvalidURIError => e
    raise ConfigurationError, "Invalid redirect URI format: #{e.message}"
  end

  validate_private_key!(normalized[:private_key])

  i[authorization_endpoint token_endpoint jwks_uri].each do |endpoint|
    if normalized.key?(endpoint) && !StringHelpers.blank?(normalized[endpoint])
      # Endpoints can be paths or full URLs
      endpoint_value = normalized[endpoint]
      unless endpoint_value.to_s.start_with?("/", "http://", "https://")
        raise ConfigurationError, "Invalid endpoint format for #{endpoint}: #{endpoint_value}"
      end
    end
  end

  normalized
end

.validate_entity_identifier!(entity_id, max_length: nil) ⇒ String

Validate entity identifier per OpenID Federation 1.0 spec Entity identifiers are URIs that identify entities in the federation

Parameters:

  • entity_id (String, nil)

    Entity identifier

  • max_length (Integer) (defaults to: nil)

    Maximum length (default: Configuration.config.max_string_length)

Returns:

  • (String)

    Validated and trimmed entity identifier

Raises:



396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/omniauth_openid_federation/validators.rb', line 396

def self.validate_entity_identifier!(entity_id, max_length: nil)
  max_length ||= ::OmniauthOpenidFederation::Configuration.config.max_string_length
  if StringHelpers.blank?(entity_id)
    raise SecurityError, "Entity identifier cannot be nil or empty"
  end

  str = entity_id.to_s.strip
  if str.empty?
    raise SecurityError, "Entity identifier cannot be empty after trimming"
  end

  validate_uri_safe!(str, max_length: max_length, allowed_schemes: ["http", "https"])

  str
end

.validate_file_path!(path, required: false) ⇒ Object

Validate that a file path exists

Parameters:

  • path (String, nil)

    The file path to validate

  • required (Boolean) (defaults to: false)

    Whether the file is required

Raises:



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/omniauth_openid_federation/validators.rb', line 59

def self.validate_file_path!(path, required: false)
  if StringHelpers.blank?(path)
    if required
      raise ConfigurationError, "File path is required"
    end
    return false
  end

  unless File.exist?(path)
    if required
      raise ConfigurationError, "File not found: #{path}"
    end
    return false
  end

  true
end

.validate_nonce(nonce, required: false) ⇒ String?

Validate and sanitize nonce parameter nonce is REQUIRED for Implicit and Hybrid flows, RECOMMENDED for Authorization Code flow

Parameters:

  • nonce (String, nil)

    Nonce value

  • required (Boolean) (defaults to: false)

    Whether nonce is required (default: false)

Returns:

  • (String, nil)

    Sanitized nonce value or nil

Raises:



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/omniauth_openid_federation/validators.rb', line 337

def self.validate_nonce(nonce, required: false)
  return nil unless nonce

  str = nonce.to_s.strip
  if str.empty?
    if required
      raise ConfigurationError, "nonce is REQUIRED but is empty after trimming"
    end
    return nil
  end

  sanitized = sanitize_request_param(str)
  if StringHelpers.blank?(sanitized)
    if required
      raise ConfigurationError, "nonce contains invalid characters"
    end
    return nil
  end

  sanitized
end

.validate_private_key!(private_key) ⇒ Object

Validate that a private key is present and valid

Parameters:

  • private_key (OpenSSL::PKey::RSA, String, nil)

    The private key to validate

Raises:



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/omniauth_openid_federation/validators.rb', line 12

def self.validate_private_key!(private_key)
  if private_key.nil?
    raise ConfigurationError, "Private key is required for signed request objects"
  end

  if private_key.is_a?(String)
    begin
      OpenSSL::PKey::RSA.new(private_key)
    rescue => e
      raise ConfigurationError, "Invalid private key format: #{e.message}"
    end
  elsif !private_key.is_a?(OpenSSL::PKey::RSA)
    raise ConfigurationError, "Private key must be an OpenSSL::PKey::RSA instance or PEM string"
  end

  true
end

.validate_redirect_uri!(redirect_uri) ⇒ String

Validate and sanitize redirect_uri per OIDC Core 1.0 spec redirect_uri is REQUIRED and must be a valid absolute URI

Parameters:

  • redirect_uri (String, nil)

    Redirect URI

Returns:

  • (String)

    Validated redirect_uri

Raises:



248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/omniauth_openid_federation/validators.rb', line 248

def self.validate_redirect_uri!(redirect_uri)
  if StringHelpers.blank?(redirect_uri)
    raise ConfigurationError, "redirect_uri is REQUIRED per OIDC Core 1.0 spec"
  end

  str = redirect_uri.to_s.strip
  if str.empty?
    raise ConfigurationError, "redirect_uri cannot be empty after trimming"
  end

  validated = validate_uri_safe!(str, allowed_schemes: ["http", "https"])
  validated.to_s
end

.validate_response_type!(response_type) ⇒ String

Validate and sanitize response_type per OIDC Core 1.0 spec response_type is REQUIRED and must be a valid value

Parameters:

  • response_type (String, nil)

    Response type

Returns:

  • (String)

    Validated response_type

Raises:



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/omniauth_openid_federation/validators.rb', line 365

def self.validate_response_type!(response_type)
  if StringHelpers.blank?(response_type)
    raise ConfigurationError, "response_type is REQUIRED per OIDC Core 1.0 spec"
  end

  str = response_type.to_s.strip
  if str.empty?
    raise ConfigurationError, "response_type cannot be empty after trimming"
  end

  sanitized = sanitize_request_param(str)
  if StringHelpers.blank?(sanitized)
    raise ConfigurationError, "response_type contains invalid characters"
  end

  valid_types = ["code", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token"]
  types = sanitized.split(" ").map(&:strip)
  unless types.all? { |t| valid_types.include?(t) || t.match?(/^[a-z_]+$/) }
    raise ConfigurationError, "response_type contains invalid value: #{sanitized}"
  end

  sanitized
end

.validate_scope!(scope, require_openid: true) ⇒ String

Validate and sanitize scope per OIDC Core 1.0 spec scope is space-delimited, case-sensitive list of ASCII string values MUST include “openid” scope value

Parameters:

  • scope (String, Array, nil)

    Scope value(s)

  • require_openid (Boolean) (defaults to: true)

    Whether to require “openid” scope (default: true)

Returns:

  • (String)

    Normalized space-separated scope string

Raises:



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/omniauth_openid_federation/validators.rb', line 270

def self.validate_scope!(scope, require_openid: true)
  if StringHelpers.blank?(scope)
    if require_openid
      raise ConfigurationError, "scope is REQUIRED and MUST include 'openid' per OIDC Core 1.0 spec"
    end
    return nil
  end

  scopes = case scope
  when Array
    scope.map(&:to_s).map(&:strip).reject { |s| StringHelpers.blank?(s) }
  when String
    scope.strip.split(" ").map(&:strip).reject { |s| StringHelpers.blank?(s) }
  else
    scope.to_s.strip.split(" ").map(&:strip).reject { |s| StringHelpers.blank?(s) }
  end

  scopes = scopes.map { |s| sanitize_request_param(s) }.compact

  if scopes.empty?
    raise ConfigurationError, "scope cannot be empty after validation"
  end

  if require_openid && !scopes.include?("openid")
    raise ConfigurationError, "scope MUST include 'openid' per OIDC Core 1.0 spec"
  end

  result = scopes.join(" ")
  max_length = ::OmniauthOpenidFederation::Configuration.config.max_string_length
  if result.length > max_length
    raise ConfigurationError, "scope exceeds maximum length of #{max_length} characters"
  end

  result
end

.validate_state!(state) ⇒ String

Validate and sanitize state parameter state is REQUIRED for CSRF protection

Parameters:

  • state (String, nil)

    State value

Returns:

  • (String)

    Sanitized state value

Raises:



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/omniauth_openid_federation/validators.rb', line 312

def self.validate_state!(state)
  if StringHelpers.blank?(state)
    raise ConfigurationError, "state is REQUIRED for CSRF protection"
  end

  str = state.to_s.strip
  if str.empty?
    raise ConfigurationError, "state cannot be empty after trimming"
  end

  sanitized = sanitize_request_param(str)
  if StringHelpers.blank?(sanitized)
    raise ConfigurationError, "state contains invalid characters"
  end

  sanitized
end

.validate_uri!(uri, required: false) ⇒ Object

Validate that a URI is valid

Parameters:

  • uri (String, URI, nil)

    The URI to validate

  • required (Boolean) (defaults to: false)

    Whether the URI is required

Raises:



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/omniauth_openid_federation/validators.rb', line 35

def self.validate_uri!(uri, required: false)
  if StringHelpers.blank?(uri)
    if required
      raise ConfigurationError, "URI is required"
    end
    return false
  end

  begin
    parsed = URI.parse(uri.to_s)
    unless parsed.is_a?(URI::HTTP) || parsed.is_a?(URI::HTTPS)
      raise ConfigurationError, "URI must be HTTP or HTTPS: #{uri}"
    end
    true
  rescue URI::InvalidURIError => e
    raise ConfigurationError, "Invalid URI format: #{e.message}"
  end
end

.validate_uri_safe!(uri_str, max_length: nil, allowed_schemes: ["http", "https"]) ⇒ Object

Validate URI for user input only (not config values) Prevents URI gem exploitation and validates scheme/length

Raises:



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/omniauth_openid_federation/validators.rb', line 151

def self.validate_uri_safe!(uri_str, max_length: nil, allowed_schemes: ["http", "https"])
  max_length ||= ::OmniauthOpenidFederation::Configuration.config.max_string_length
  raise SecurityError, "URI cannot be nil" if uri_str.nil?

  original_str = uri_str.to_s
  sanitized = original_str.gsub(/[^\x20-\x7E]/, "")
  raise SecurityError, "URI contains invalid characters (only printable ASCII allowed)" if sanitized != original_str

  str = sanitized.strip
  raise SecurityError, "URI cannot be empty" if str.empty?
  raise SecurityError, "URI exceeds maximum length of #{max_length} characters" if str.length > max_length

  begin
    parsed = URI.parse(str)
  rescue URI::InvalidURIError => e
    raise SecurityError, "Invalid URI format: #{e.message}"
  end

  unless parsed.scheme && allowed_schemes.include?(parsed.scheme.downcase)
    raise SecurityError, "URI scheme must be one of: #{allowed_schemes.join(", ")}"
  end

  unless parsed.is_a?(URI::HTTP) || parsed.is_a?(URI::HTTPS)
    raise SecurityError, "URI must be HTTP or HTTPS"
  end

  raise SecurityError, "URI host cannot be empty" if StringHelpers.blank?(parsed.host)

  parsed
end