Class: OmniauthOpenidFederation::Jwks::Rotate

Inherits:
Object
  • Object
show all
Defined in:
lib/omniauth_openid_federation/jwks/rotate.rb,
sig/jwks.rbs

Overview

JWKS rotation service

Examples:

Rotate JWKS for a provider

OmniauthOpenidFederation::Jwks::Rotate.run(
  "https://provider.example.com/.well-known/jwks.json",
  entity_statement_path: "config/provider-entity-statement.jwt"
)

Class Method Summary collapse

Class Method Details

.run(jwks_uri, entity_statement_path: nil) ⇒ Hash

Rotate JWKS cache for a provider This is useful for background jobs to proactively refresh keys

Parameters:

  • jwks_uri (String)

    The JWKS URI to refresh

  • entity_statement_path (String, nil) (defaults to: nil)

    Path to entity statement file (optional)

Returns:

  • (Hash)

    The refreshed JWKS hash

Raises:



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/omniauth_openid_federation/jwks/rotate.rb', line 38

def self.run(jwks_uri, entity_statement_path: nil)
  if entity_statement_path
    # Validate file path to prevent path traversal
    # Allow absolute paths that exist (for temp files in tests) to skip directory validation
    # For absolute paths that don't exist, still validate they're not path traversal, then check existence
    path_str = entity_statement_path.to_s
    is_absolute = path_str.start_with?("/", "~")

    if is_absolute && File.exist?(entity_statement_path)
      validated_path = entity_statement_path
    else
      # For absolute paths, validate path traversal but allow outside allowed_dirs
      # For relative paths, validate against allowed directories
      if is_absolute
        # Validate path traversal for absolute paths, but don't require it to be in allowed_dirs
        begin
          validated_path = Utils.validate_file_path!(
            entity_statement_path,
            allowed_dirs: nil  # Allow absolute paths outside config directory
          )
        rescue SecurityError => e
          # Path traversal attempt - raise SecurityError
          Logger.error("[Jwks::Rotate] #{e.message}")
          raise SecurityError, e.message
        end
      else
        # Relative path - must be in allowed directories
        begin
          config = Configuration.config
          allowed_dirs = if defined?(Rails) && Rails.root
            [Rails.root.join("config").to_s]
          elsif config.root_path
            [File.join(config.root_path, "config")]
          end

          validated_path = Utils.validate_file_path!(
            entity_statement_path,
            allowed_dirs: allowed_dirs
          )
        rescue SecurityError => e
          Logger.error("[Jwks::Rotate] #{e.message}")
          raise SecurityError, e.message
        end
      end

      unless File.exist?(validated_path)
        sanitized_path = Utils.sanitize_path(validated_path)
        Logger.warn("[Jwks::Rotate] Entity statement file not found: #{sanitized_path}")
        raise ConfigurationError, "Entity statement file not found: #{sanitized_path}"
      end
    end

    # Try to use signed JWKS if entity statement is available
    begin
      parsed = Federation::EntityStatementHelper.parse_for_signed_jwks(validated_path)
      if parsed && StringHelpers.present?(parsed[:signed_jwks_uri])
        return Federation::SignedJWKS.fetch!(
          parsed[:signed_jwks_uri],
          parsed[:entity_jwks],
          force_refresh: true
        )
      end
    rescue SecurityError
      raise
    rescue
      Logger.warn("[Jwks::Rotate] Failed to use signed JWKS, falling back to standard JWKS")
    end

    # Fallback to standard JWKS with entity statement keys
    entity_statement_keys = EntityStatementReader.fetch_keys(
      entity_statement_path: validated_path
    )
    return Fetch.run(
      jwks_uri,
      entity_statement_keys: entity_statement_keys,
      force_refresh: true
    )
  end

  # Use standard JWKS
  Fetch.run(jwks_uri, force_refresh: true)
end