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



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/omniauth_openid_federation/validators.rb', line 225

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



158
159
160
161
162
163
164
165
# File 'lib/omniauth_openid_federation/validators.rb', line 158

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



170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/omniauth_openid_federation/validators.rb', line 170

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_allowed_acr_value!(acr_value, allowed_acr_values) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/omniauth_openid_federation/validators.rb', line 52

def self.validate_allowed_acr_value!(acr_value, allowed_acr_values)
  allowed = Array(allowed_acr_values).map(&:to_s).map(&:strip).reject { |value| StringHelpers.blank?(value) }
  return true if allowed.empty?

  token_acr = acr_value.to_s.strip
  if StringHelpers.blank?(token_acr) || !allowed.include?(token_acr)
    raise ValidationError,
      "ID token acr mismatch: expected one of [#{allowed.join(", ")}], got '#{acr_value}'"
  end

  true
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:



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/omniauth_openid_federation/validators.rb', line 259

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:



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/omniauth_openid_federation/validators.rb', line 117

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:



431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/omniauth_openid_federation/validators.rb', line 431

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:



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/omniauth_openid_federation/validators.rb', line 94

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:



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/omniauth_openid_federation/validators.rb', line 372

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, min_bits: Constants::MIN_RSA_KEY_BITS) ⇒ 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
29
30
31
32
33
34
35
36
# File 'lib/omniauth_openid_federation/validators.rb', line 12

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

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

  if min_bits.to_i.positive? && normalized_key.n.num_bits < min_bits.to_i
    raise ConfigurationError,
      "RSA private key must be at least #{min_bits} bits (got #{normalized_key.n.num_bits} bits)"
  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:



283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/omniauth_openid_federation/validators.rb', line 283

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_required_request_object_claims!(claims, required_names) ⇒ Object

Raises:



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

def self.validate_required_request_object_claims!(claims, required_names)
  required = Array(required_names).map(&:to_s).uniq
  return true if required.empty?

  normalized_claims = (claims || {}).each_with_object({}) do |(key, value), hash|
    hash[key.to_s] = value
  end

  missing = required.reject { |name| StringHelpers.present?(normalized_claims[name]) }
  return true if missing.empty?

  raise ConfigurationError, "Missing required request object claims: #{missing.join(", ")}"
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:



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/omniauth_openid_federation/validators.rb', line 400

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:



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/omniauth_openid_federation/validators.rb', line 305

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:



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/omniauth_openid_federation/validators.rb', line 347

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:



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/omniauth_openid_federation/validators.rb', line 70

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:



186
187
188
189
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
# File 'lib/omniauth_openid_federation/validators.rb', line 186

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