Module: StandardId::Oauth::DiscoveryResolver
- Defined in:
- lib/standard_id/oauth/discovery_resolver.rb
Overview
Resolves the three request-dependent inputs of a discovery document: the issuer, the endpoint base, and the overrides.
Lives apart from DiscoveryDocument so the builder stays a pure function of its arguments (and stays trivially testable without a request), while all the "where am I actually mounted" reasoning is in one place.
Constant Summary collapse
- REQUEST_DERIVED =
Values of
discovery_endpoint_basethat mean "derive it from the request". Compared as strings so:requestand"request"both work — this is set from an initializer and the symbol/string distinction is not something a config file should have to get right. %w[request].freeze
- MOUNT_PATH_PARAM =
Route default the routing helper uses to carry the mount path to a root-served document. Named rather than positional so it cannot collide with a host's own params.
:standard_id_mount_path
Class Method Summary collapse
-
.endpoint_base(request: nil) ⇒ String?
Base URL the advertised endpoints hang off, from
config.oauth.discovery_endpoint_base:. -
.issuer ⇒ String?
The issuer, verbatim from config.
-
.metadata_overrides(request: nil, endpoint_base: nil) ⇒ Hash
Resolve
config.oauth.discovery_metadata_overridesfor this request. -
.mount_path(request) ⇒ String
The path ApiEngine is mounted at, as seen from this request.
-
.normalize_path(path) ⇒ Object
"" or "/foo" — never "/" and never a trailing slash, so string concatenation against an origin cannot produce a double slash.
-
.resolve(request: nil) ⇒ Hash
Everything a well-known controller needs, in one call.
Class Method Details
.endpoint_base(request: nil) ⇒ String?
Base URL the advertised endpoints hang off, from
config.oauth.discovery_endpoint_base:
nil (default) — the issuer. EXACTLY the behaviour before this option
existed, so an upgrade changes no document. Correct
already for an app whose issuer carries the mount path
(`https://host/auth/api`); wrong, and the bug this
option exists for, when it does not.
:request — `request.base_url` + the detected mount path. This is
what an app mounting ApiEngine under a prefix wants.
String — used verbatim.
callable — called with `(request:)`. The escape hatch for a proxy
that rewrites `request.base_url`, and for an app that
mounts ApiEngine more than once (under host constraints,
say) where no single detected path is right.
Request-derived is deliberately OPT-IN rather than the default. Making it the default would silently rewrite the document of every app whose issuer host differs from the host serving the request — which is exactly the split-host setup a separate issuer exists to express.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/standard_id/oauth/discovery_resolver.rb', line 51 def endpoint_base(request: nil) configured = StandardId.config.oauth.discovery_endpoint_base return issuer if configured.blank? if configured.respond_to?(:call) resolved = configured.call(request: request) return resolved.present? ? resolved.to_s.chomp("/") : issuer end return configured.to_s.chomp("/") unless REQUEST_DERIVED.include?(configured.to_s) return issuer if request.nil? "#{request.base_url}#{mount_path(request)}".chomp("/") end |
.issuer ⇒ String?
The issuer, verbatim from config. Never derived from the request, never
overridable: RFC 8414 §2 makes it a stable security identifier that
clients match byte-for-byte against their discovery URL and against the
iss claim of issued tokens.
24 25 26 |
# File 'lib/standard_id/oauth/discovery_resolver.rb', line 24 def issuer StandardId.config.issuer end |
.metadata_overrides(request: nil, endpoint_base: nil) ⇒ Hash
Resolve config.oauth.discovery_metadata_overrides for this request.
Values may be static or callable. A callable receives one Hash argument so members can be built from the request without each app re-deriving the origin:
{
# host-owned shim that injects the audience before handing off to the
# engine's /authorize
authorization_endpoint: ->(ctx) { "#{ctx[:origin]}/oauth/authorize" },
# deliberately narrower than what the server can mint
scopes_supported: %w[mcp mcp:read],
# must mirror the host's DCR policy, or spec-following clients
# register in a way that policy then rejects
token_endpoint_auth_methods_supported: %w[none],
# remove a member entirely
grant_types_supported: nil
}
Context keys: :origin (scheme+host+port, no path), :endpoint_base,
:issuer, :request.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/standard_id/oauth/discovery_resolver.rb', line 119 def (request: nil, endpoint_base: nil) configured = StandardId.config.oauth. return {} if configured.blank? context = { origin: request&.base_url, endpoint_base: endpoint_base, issuer: issuer, request: request } configured.to_h.each_with_object({}) do |(key, value), acc| acc[key.to_sym] = value.respond_to?(:call) ? value.call(context) : value end end |
.mount_path(request) ⇒ String
The path ApiEngine is mounted at, as seen from this request.
Two cases, and the difference is why this is not a config lookup:
* The document is served from INSIDE the engine mount (the gem's own
`scope ".well-known"` routes). Rails sets SCRIPT_NAME to the mount
prefix, so `request.script_name` IS the mount path, exactly, with no
detection or guessing. This is the case that used to produce 404-ing
endpoint URLs.
* The document is served from a ROOT route drawn by
`standard_id_well_known_routes` (RFC 8615 clients probe the origin
root, which is outside any engine mount). There `script_name` is
empty, so the helper stamps the mount path it was given into the
route defaults and we read it back from `params`.
84 85 86 87 88 89 |
# File 'lib/standard_id/oauth/discovery_resolver.rb', line 84 def mount_path(request) from_defaults = request.params[MOUNT_PATH_PARAM].presence return normalize_path(from_defaults) if from_defaults normalize_path(request.script_name) end |
.normalize_path(path) ⇒ Object
"" or "/foo" — never "/" and never a trailing slash, so string concatenation against an origin cannot produce a double slash.
150 151 152 153 154 155 |
# File 'lib/standard_id/oauth/discovery_resolver.rb', line 150 def normalize_path(path) normalized = path.to_s.chomp("/") return "" if normalized.empty? normalized.start_with?("/") ? normalized : "/#{normalized}" end |
.resolve(request: nil) ⇒ Hash
Everything a well-known controller needs, in one call.
138 139 140 141 142 143 144 145 146 |
# File 'lib/standard_id/oauth/discovery_resolver.rb', line 138 def resolve(request: nil) base = endpoint_base(request: request) { issuer: issuer, endpoint_base: base, overrides: (request: request, endpoint_base: base) } end |