Class: LcpRuby::Authentication::ProviderRegistry

Inherits:
Object
  • Object
show all
Defined in:
lib/lcp_ruby/authentication/provider_registry.rb

Overview

Loads and caches the auth.yml provider registry. Single source of truth for OmniAuthBuilder, the adaptive login view, the callbacks controller, and (Phase 2) the bearer middleware.

Lifecycle:

- First access (`all`, `oidc_enabled?`, ...) reads & validates auth.yml.
- Result is memoized; subsequent accesses are O(1) hash lookups.
- `reload!` invalidates both the provider list and the discovery cache.
  Used by tests and by Engine.reload!.

Constant Summary collapse

DISCOVERY_TTL =

seconds

3600

Class Method Summary collapse

Class Method Details

.allObject

Returns Array<Provider>. Triggers a one-time load of auth.yml on first call.



24
25
26
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 24

def all
  @all ||= load!
end

.auth_yml_pathObject

Path to auth.yml. Falls back to LcpRuby.configuration.metadata_path.



145
146
147
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 145

def auth_yml_path
  File.join(LcpRuby.configuration..to_s, "auth.yml")
end

.configured?Boolean

Returns:

  • (Boolean)


149
150
151
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 149

def configured?
  File.exist?(auth_yml_path)
end

.defaultObject

The provider that should be highlighted on the login page. Falls back to the first listed provider when no default_provider key is set.



39
40
41
42
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 39

def default
  name = raw_config["default_provider"]&.to_s
  (name && find_by(name)) || all.first
end

.default_auto_redirect?Boolean

Returns:

  • (Boolean)


52
53
54
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 52

def default_auto_redirect?
  !!raw_config["default_provider_auto_redirect"]
end

.devise_enabled?Boolean

Whether at least one Devise (built_in) provider entry exists.

Returns:

  • (Boolean)


57
58
59
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 57

def devise_enabled?
  all.any?(&:devise?)
end

.discovery_for(provider) ⇒ Object

Returns the discovery document (Hash) for an OIDC provider, fetched lazily and cached for DISCOVERY_TTL seconds. Used by OmniAuthBuilder and the RP-initiated logout flow.

‘Concurrent::Map#compute` holds a per-key lock for the duration of the block, so two threads racing on the same provider serialise on the fetch — the first issues the HTTP request, the second sees the populated entry and skips. Without this, both threads would miss the cache and both would fetch (idempotent but wasteful, and worse under JWKS where misses are an attack-amplification vector).



132
133
134
135
136
137
138
139
140
141
142
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 132

def discovery_for(provider)
  now = Time.now.to_i
  entry = discovery_cache.compute(provider.name) do |existing|
    if existing && existing[:fetched_at] + DISCOVERY_TTL > now
      existing
    else
      { doc: fetch_discovery(provider.discovery_url), fetched_at: now }
    end
  end
  entry[:doc]
end

.find(name) ⇒ Object



28
29
30
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 28

def find(name)
  find_by(name) or raise UnknownProvider.new(name)
end

.find_by(name) ⇒ Object

Non-raising sibling of ‘find`. Returns nil when the provider isn’t registered.



33
34
35
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 33

def find_by(name)
  all.find { |p| p.name == name.to_s }
end

.find_by_issuer(iss) ⇒ Object

Returns the OIDC provider whose discovery ‘issuer` matches `iss`, or nil. Iterates `oidc_providers`, but `discovery_for` is itself cached for an hour — so per-request cost is N hash lookups rather than N HTTP calls. A misconfigured provider must not block tokens from other working providers, so transient discovery errors are logged and skipped.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 95

def find_by_issuer(iss)
  return nil if iss.nil? || iss.to_s.empty?

  oidc_providers.find do |p|
    doc = discovery_for(p)
    doc.is_a?(Hash) && doc["issuer"] == iss
  rescue ConfigurationError, SocketError, Timeout::Error,
         Net::HTTPError, OpenSSL::SSL::SSLError, JSON::ParserError => e
    Rails.logger.info(
      "[lcp_ruby] ProviderRegistry skipping provider '#{p.name}' " \
      "in issuer match: #{e.class}: #{e.message}"
    ) if defined?(Rails)
    false
  end
end

.load_errorObject

Last ConfigurationError encountered while loading auth.yml, or nil when the load succeeded (or hasn’t been attempted). Set by ‘load!` alongside the raise so non-raising introspection is possible —`ConfigurationValidator#validate_auth_yml` reads this to surface auth.yml problems in the validate report without re-parsing YAML.



85
86
87
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 85

def load_error
  @load_error
end

.oidc_enabled?Boolean

Returns:

  • (Boolean)


48
49
50
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 48

def oidc_enabled?
  oidc_providers.any?
end

.oidc_providersObject



44
45
46
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 44

def oidc_providers
  @oidc_providers ||= all.select(&:oidc?)
end

.password_sign_in_allowed?Boolean

Whether the login page should render the password form. True when there’s a Devise provider entry, OR when auth.yml is absent entirely (legacy :built_in-only setups). Drives both ‘SessionsController#new` and the `#create` guard so they can’t disagree.

Returns:

  • (Boolean)


65
66
67
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 65

def 
  all.empty? || devise_enabled?
end

.reload!Object

Force-refresh of providers and discovery cache. Tests call this in ‘before` hooks after stubbing auth.yml on disk.



71
72
73
74
75
76
77
78
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 71

def reload!
  @all = nil
  @oidc_providers = nil
  @raw_root = nil
  @load_error = nil
  discovery_cache.clear
  self
end

.replace_for_test!(replacement) ⇒ Object

Test seam: replace a frozen Provider in the memoized list with a variant produced via ‘provider.with(…)`. Ensures specs don’t need to know the internal storage shape and never mutate frozen registry entries directly.



115
116
117
118
119
120
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 115

def replace_for_test!(replacement)
  load! unless @all
  @all = @all.map { |p| p.name == replacement.name ? replacement : p }
  @oidc_providers = nil
  replacement
end