Class: Verikloak::Discovery

Inherits:
Object
  • Object
show all
Defined in:
lib/verikloak/discovery.rb

Overview

Fetches and caches the OpenID Connect Discovery document.

This class retrieves the discovery metadata from an OpenID Connect provider (e.g., Keycloak) using the .well-known/openid-configuration endpoint. It validates required fields such as jwks_uri and issuer, and supports:

  • Dependency Injection of Faraday connection for testing and middleware
  • In-memory caching with configurable TTL
  • Thread safety via Mutex
  • Automatic handling of common HTTP statuses (including multi-hop redirects)

Thread-safety

#fetch! is synchronized, so concurrent callers share the same cached value and refresh.

Errors

Raises DiscoveryError with one of the following codes:

  • invalid_discovery_url
  • discovery_metadata_fetch_failed
  • discovery_metadata_invalid
  • discovery_redirect_error

Examples:

Basic usage

discovery = Verikloak::Discovery.new(
  discovery_url: "https://keycloak.example.com/realms/demo/.well-known/openid-configuration"
)
json = discovery.fetch!
json["issuer"]   #=> "https://keycloak.example.com/realms/demo"
json["jwks_uri"] #=> "https://keycloak.example.com/realms/demo/protocol/openid-connect/certs"

Constant Summary collapse

REQUIRED_FIELDS =

Required keys that must be present in the discovery document.

Returns:

  • (Array<String>)
%w[jwks_uri issuer].freeze

Instance Method Summary collapse

Constructor Details

#initialize(discovery_url:, connection: Verikloak::HTTP.default_connection, cache_ttl: 3600, allow_http: false) ⇒ Discovery

Returns a new instance of Discovery.

Parameters:

  • discovery_url (String)

    The full URL to the .well-known/openid-configuration.

  • connection (Faraday::Connection) (defaults to: Verikloak::HTTP.default_connection)

    Optional Faraday client (for DI/tests).

  • cache_ttl (Integer) (defaults to: 3600)

    Cache TTL in seconds (default: 3600).

  • allow_http (Boolean) (defaults to: false)

    When false (default), raises on plain HTTP URLs. Set true for local development only.

Raises:



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/verikloak/discovery.rb', line 49

def initialize(discovery_url:, connection: Verikloak::HTTP.default_connection, cache_ttl: 3600, allow_http: false)
  normalized_url = SafeUrl.normalize(discovery_url)

  unless normalized_url
    raise DiscoveryError.new('Invalid discovery URL: must be a non-empty HTTP(S) URL',
                             code: 'invalid_discovery_url')
  end

  if SafeUrl.insecure_http?(normalized_url, allow_http: allow_http)
    raise DiscoveryError.new(
      'Discovery URL must use HTTPS. Set allow_http: true to permit plain HTTP (development only).',
      code: 'insecure_discovery_url'
    )
  end

  @discovery_url = normalized_url
  @conn          = connection
  @cache_ttl     = cache_ttl
  @allow_http    = allow_http
  @cached_json   = nil
  @fetched_at    = nil
  @mutex         = Mutex.new
end

Instance Method Details

#fetch!Hash

Fetches and parses the discovery document, using the in-memory cache if fresh.

Cache freshness is determined by cache_ttl from initialization.

Returns:

  • (Hash)

    Parsed JSON object containing discovery metadata.

Raises:

  • (DiscoveryError)

    if the request fails, the response is invalid, or required fields are missing.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/verikloak/discovery.rb', line 79

def fetch!
  @mutex.synchronize do
    # Return cached if within TTL
    return @cached_json if @cached_json && (Time.now - @fetched_at) < @cache_ttl

    # Fetch fresh document
    json = with_error_handling { fetch_and_parse_json_from_url }
    validate_required_fields!(json)

    # Update cache
    @cached_json = json
    @fetched_at  = Time.now
    json
  end
end