Module: OmniauthOpenidFederation::Utils

Defined in:
lib/omniauth_openid_federation/utils.rb

Class Method Summary collapse

Class Method Details

.build_endpoint_url(issuer_uri, endpoint_path) ⇒ String

Build full endpoint URL from issuer and endpoint path

Parameters:

Returns:

  • (String)

    Full endpoint URL



54
55
56
57
58
59
60
61
62
# File 'lib/omniauth_openid_federation/utils.rb', line 54

def self.build_endpoint_url(issuer_uri, endpoint_path)
  return endpoint_path if endpoint_path.to_s.start_with?("http://", "https://")

  issuer_str = issuer_uri.to_s
  issuer_str = issuer_str.chomp("/")
  path = endpoint_path.to_s
  path = "/#{path}" unless path.start_with?("/")
  "#{issuer_str}#{path}"
end

.build_entity_statement_url(issuer_uri, entity_statement_endpoint: nil) ⇒ String

Build full entity statement URL from issuer and endpoint path

Parameters:

  • issuer_uri (String, URI)
  • entity_statement_endpoint (String, nil) (defaults to: nil)

    Entity statement endpoint path (e.g., "/.well-known/openid-federation")

Returns:

  • (String)

    Full entity statement URL



69
70
71
72
# File 'lib/omniauth_openid_federation/utils.rb', line 69

def self.build_entity_statement_url(issuer_uri, entity_statement_endpoint: nil)
  endpoint = entity_statement_endpoint || "/.well-known/openid-federation"
  build_endpoint_url(issuer_uri, endpoint)
end

.extract_jwks_from_entity_statement(entity_statement_content) ⇒ Hash?

Extract JWKS from entity statement JWT

Parameters:

  • entity_statement_content (String)

    Entity statement JWT string

Returns:

  • (Hash, nil)

    JWKS hash with "keys" array, or nil if extraction fails



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/omniauth_openid_federation/utils.rb', line 149

def self.extract_jwks_from_entity_statement(entity_statement_content)
  require "json"
  require "base64"

  return nil unless valid_jwt_format?(entity_statement_content)

  jwt_parts = entity_statement_content.split(".")
  return nil unless jwt_parts.length == 3

  begin
    payload = JSON.parse(Base64.urlsafe_decode64(jwt_parts[1]))
    entity_jwks = payload["jwks"] || payload[:jwks] || {}
    return nil if entity_jwks.empty?

    keys = entity_jwks["keys"] || entity_jwks[:keys] || []
    return nil if keys.empty?

    {keys: Array(keys)}
  rescue => e
    OmniauthOpenidFederation::Logger.warn("[Utils] Failed to extract JWKS from entity statement: #{e.message}")
    nil
  end
end

.rsa_key_to_jwk(key, use: "sig") ⇒ Hash

Convert RSA key to JWK format

Parameters:

  • key (OpenSSL::PKey::RSA)

    RSA private or public key

  • use (String, nil) (defaults to: "sig")

    Key use ("sig" for signing, "enc" for encryption, nil for both)

Returns:

  • (Hash)

    JWK hash with kty, kid, n, e, and optionally use



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/omniauth_openid_federation/utils.rb', line 123

def self.rsa_key_to_jwk(key, use: "sig")
  require "digest"
  require "base64"

  n = Base64.urlsafe_encode64(key.n.to_s(2), padding: false)
  e = Base64.urlsafe_encode64(key.e.to_s(2), padding: false)

  public_key_pem = key.public_key.to_pem
  kid = Digest::SHA256.hexdigest(public_key_pem)[0, 16]

  jwk = {
    kty: "RSA",
    kid: kid,
    n: n,
    e: e
  }

  jwk[:use] = use if use

  jwk
end

.sanitize_path(path) ⇒ String

Sanitize file path for error messages (only show filename, not full path)

Parameters:

  • path (String, nil)

    The file path

Returns:

  • (String)

    Sanitized path (filename only)



30
31
32
33
# File 'lib/omniauth_openid_federation/utils.rb', line 30

def self.sanitize_path(path)
  return "[REDACTED]" if StringHelpers.blank?(path)
  File.basename(path)
end

.sanitize_uri(uri) ⇒ String

Sanitize URI for error messages (only show scheme and host)

Parameters:

  • uri (String, nil)

    The URI

Returns:

  • (String)

    Sanitized URI



39
40
41
42
43
44
45
46
47
# File 'lib/omniauth_openid_federation/utils.rb', line 39

def self.sanitize_uri(uri)
  return "[REDACTED]" if StringHelpers.blank?(uri)
  begin
    parsed = URI.parse(uri)
    "#{parsed.scheme}://#{parsed.host}/[REDACTED]"
  rescue URI::InvalidURIError
    "[REDACTED]"
  end
end

.to_indifferent_hash(value) ⇒ Hash, HashWithIndifferentAccess

Convert hash to HashWithIndifferentAccess if available

Parameters:

  • hash (Hash, Object)

    The hash to convert

Returns:

  • (Hash, HashWithIndifferentAccess)

    Converted hash



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/omniauth_openid_federation/utils.rb', line 10

def self.to_indifferent_hash(value)
  hash = if value.is_a?(Hash)
    value
  elsif value.respond_to?(:to_h)
    value.to_h
  else
    {}
  end

  if defined?(ActiveSupport::HashWithIndifferentAccess)
    ActiveSupport::HashWithIndifferentAccess.new(hash)
  else
    hash
  end
end

.valid_jwt_format?(str) ⇒ Boolean

Validate JWT format (must have exactly 3 parts separated by dots)

Parameters:

  • str (String)

    The string to validate

Returns:

  • (Boolean)

    true if valid JWT format, false otherwise



112
113
114
115
116
# File 'lib/omniauth_openid_federation/utils.rb', line 112

def self.valid_jwt_format?(str)
  return false unless str.is_a?(String)
  parts = str.split(".")
  parts.length == 3 && parts.all? { |p| p.length > 0 }
end

.validate_file_path!(path, allowed_dirs: nil) ⇒ String

Validate file path to prevent path traversal attacks

Parameters:

  • path (String, Pathname)

    The file path to validate

  • allowed_dirs (Array<String>, nil) (defaults to: nil)

    Allowed base directories (optional)

Returns:

  • (String)

    Resolved absolute path

Raises:

  • (SecurityError)

    If path traversal is detected or path is outside allowed directories



80
81
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
# File 'lib/omniauth_openid_federation/utils.rb', line 80

def self.validate_file_path!(path, allowed_dirs: nil)
  raise SecurityError, "File path cannot be nil" if path.nil?

  path_str = path.to_s
  raise SecurityError, "File path cannot be empty" if path_str.empty?

  if path_str.include?("..") || path_str.include?("~")
    raise SecurityError, "Path traversal detected in: #{sanitize_path(path_str)}"
  end

  resolved = File.expand_path(path_str)

  # Validate it's within allowed directories if specified
  # When allowed_dirs is nil, we trust the developer to pass appropriate paths
  # Path traversal protection (.. and ~) is still enforced above
  if allowed_dirs && !allowed_dirs.empty?
    allowed = allowed_dirs.any? do |dir|
      expanded_dir = File.expand_path(dir)
      resolved.start_with?(expanded_dir)
    end
    unless allowed
      raise SecurityError, "File path outside allowed directories: #{sanitize_path(path)}"
    end
  end

  resolved
end