Class: LcpRuby::Authentication::ProviderRegistry
- Inherits:
-
Object
- Object
- LcpRuby::Authentication::ProviderRegistry
- 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
-
.all ⇒ Object
Returns Array<Provider>.
-
.auth_yml_path ⇒ Object
Path to auth.yml.
- .configured? ⇒ Boolean
-
.default ⇒ Object
The provider that should be highlighted on the login page.
- .default_auto_redirect? ⇒ Boolean
-
.devise_enabled? ⇒ Boolean
Whether at least one Devise (built_in) provider entry exists.
-
.discovery_for(provider) ⇒ Object
Returns the discovery document (Hash) for an OIDC provider, fetched lazily and cached for DISCOVERY_TTL seconds.
- .find(name) ⇒ Object
-
.find_by(name) ⇒ Object
Non-raising sibling of ‘find`.
-
.find_by_issuer(iss) ⇒ Object
Returns the OIDC provider whose discovery ‘issuer` matches `iss`, or nil.
-
.load_error ⇒ Object
Last ConfigurationError encountered while loading auth.yml, or nil when the load succeeded (or hasn’t been attempted).
- .oidc_enabled? ⇒ Boolean
- .oidc_providers ⇒ Object
-
.password_sign_in_allowed? ⇒ Boolean
Whether the login page should render the password form.
-
.reload! ⇒ Object
Force-refresh of providers and discovery cache.
-
.replace_for_test!(replacement) ⇒ Object
Test seam: replace a frozen Provider in the memoized list with a variant produced via ‘provider.with(…)`.
Class Method Details
.all ⇒ Object
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_path ⇒ Object
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
149 150 151 |
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 149 def configured? File.exist?(auth_yml_path) end |
.default ⇒ Object
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
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.
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.}" ) if defined?(Rails) false end end |
.load_error ⇒ Object
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
48 49 50 |
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 48 def oidc_enabled? oidc_providers.any? end |
.oidc_providers ⇒ Object
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.
65 66 67 |
# File 'lib/lcp_ruby/authentication/provider_registry.rb', line 65 def password_sign_in_allowed? 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 |