standard_id 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,6 +52,30 @@ module StandardId
52
52
  SUPPORTED_ALGORITHMS[algorithm] || raise(ArgumentError, "Unsupported algorithm: #{algorithm}. Supported: #{SUPPORTED_ALGORITHMS.keys.join(', ')}")
53
53
  end
54
54
 
55
+ # Whether decode REQUIRES a matching `iss`. `nil` (the default) follows the
56
+ # issuer, which is the historic behaviour: verification on exactly when an
57
+ # issuer is configured.
58
+ #
59
+ # The override exists so an app can START MINTING an `iss` without yet
60
+ # REQUIRING one. Coupled, adopting an issuer is a flag day — every token
61
+ # already in flight was minted without the claim, so enabling the issuer
62
+ # rejects all of them at once, refresh tokens included. Decoupled, the app
63
+ # sets `verify_issuer = false`, waits out `refresh_token_lifetime`, and then
64
+ # drops the override with no window in which valid tokens are refused.
65
+ def self.verify_issuer?
66
+ configured = StandardId.config.verify_issuer
67
+ return StandardId.config.issuer.present? if configured.nil?
68
+
69
+ if configured && StandardId.config.issuer.blank?
70
+ raise StandardId::ConfigurationError,
71
+ "verify_issuer is true but no issuer is configured — decode would " \
72
+ "verify against nil and accept any token. Set config.issuer, or " \
73
+ "leave verify_issuer nil to follow it."
74
+ end
75
+
76
+ configured
77
+ end
78
+
55
79
  def self.asymmetric?
56
80
  algorithm_config[:type] == :asymmetric
57
81
  end
@@ -152,7 +176,7 @@ module StandardId
152
176
  def self.decode(token, allowed_audiences: nil)
153
177
  options = { algorithms: [algorithm] }
154
178
 
155
- if StandardId.config.issuer.present?
179
+ if StandardId.config.issuer.present? && verify_issuer?
156
180
  options[:iss] = StandardId.config.issuer
157
181
  options[:verify_iss] = true
158
182
  end
@@ -5,28 +5,57 @@ module StandardId
5
5
  # * /.well-known/oauth-authorization-server (RFC 8414)
6
6
  #
7
7
  # Both well-known controllers render this single builder so the two
8
- # documents cannot drift. Endpoint URLs are derived from the configured
9
- # issuer.
8
+ # documents cannot drift.
10
9
  #
11
- # NOTE on mounting (RFC 8414 caveat): the ApiEngine is consumer-mounted at
12
- # a sub-path (e.g. `/auth/api`), so the gem can only serve
13
- # `/auth/api/.well-known/oauth-authorization-server`. A strict RFC 8414
14
- # client that derives a *root-anchored* metadata URL from a path-carrying
15
- # issuer would probe `<host>/.well-known/oauth-authorization-server/auth/api`,
16
- # which falls outside any engine mount. Hosts that need the root-anchored
17
- # form must add their own root route — the gem cannot.
10
+ # ## `issuer` and the endpoint base are two different things
11
+ #
12
+ # This used to derive every endpoint from the issuer
13
+ # (`base = issuer.to_s.chomp("/")`), which is wrong for any app mounting
14
+ # `ApiEngine` under a prefix its issuer does not carry: the document
15
+ # advertised `<issuer>/oauth/token` while the endpoint actually lived at
16
+ # `<origin>/api/oauth/token`. Every consuming app hand-rolled a replacement
17
+ # controller because of it.
18
+ #
19
+ # The two are now separate, and must stay separate:
20
+ #
21
+ # * `issuer` is a stable **security identifier** (RFC 8414 §2). Clients
22
+ # compare it byte-for-byte with the URL they used for discovery and with
23
+ # the `iss` claim of issued tokens. It is never derived from the request
24
+ # and can never be overridden — see .apply_overrides!.
25
+ # * `endpoint_base` is where the endpoints actually are. It defaults to the
26
+ # issuer, which preserves the previous behaviour exactly for apps whose
27
+ # issuer already carries the mount path.
28
+ #
29
+ # ## Overrides
30
+ #
31
+ # Some members are not derivable at all — a host-owned authorization shim
32
+ # that injects an audience, a scope list deliberately narrower than what the
33
+ # server can mint, an auth-method list that must mirror the host's dynamic
34
+ # registration policy. `overrides` covers those; see
35
+ # StandardId::Oauth::DiscoveryResolver for the config surface.
18
36
  module DiscoveryDocument
19
37
  module_function
20
38
 
21
- # @param issuer [String] the configured issuer (e.g. "https://auth.example.com")
39
+ # @param issuer [String] the configured issuer, emitted verbatim
40
+ # @param endpoint_base [String, nil] base URL for the endpoints. Defaults
41
+ # to `issuer` — the previous behaviour — so existing callers are
42
+ # unaffected.
22
43
  # @param registration_enabled [Boolean] when true, advertises the RFC 7591
23
44
  # dynamic client registration endpoint. The well-known controllers pass
24
45
  # `StandardId.config.oauth.dynamic_registration_enabled` here, so the
25
46
  # `registration_endpoint` is emitted only when DCR is turned on. Defaults
26
47
  # to false so callers (and tests) that omit it get no registration_endpoint.
48
+ # @param introspection_enabled [Boolean] when true, advertises the RFC 7662
49
+ # introspection endpoint. The well-known controllers pass
50
+ # `StandardId.config.oauth.introspection_enabled`, so it is advertised only
51
+ # when the endpoint actually exists (the controller 404s when off).
52
+ # Defaults to false, matching registration_enabled.
53
+ # @param overrides [Hash] members to replace, add, or (with a nil value)
54
+ # remove. Already resolved — callables are evaluated by the caller.
27
55
  # @return [Hash]
28
- def build(issuer, registration_enabled: false)
29
- base = issuer.to_s.chomp("/")
56
+ def build(issuer, endpoint_base: nil, registration_enabled: false,
57
+ introspection_enabled: false, overrides: {})
58
+ base = (endpoint_base.presence || issuer).to_s.chomp("/")
30
59
 
31
60
  doc = {
32
61
  issuer: issuer,
@@ -55,6 +84,38 @@ module StandardId
55
84
  doc[:jwks_uri] = "#{base}/.well-known/jwks.json" if StandardId::JwtService.asymmetric?
56
85
 
57
86
  doc[:registration_endpoint] = "#{base}/oauth/register" if registration_enabled
87
+ doc[:introspection_endpoint] = "#{base}/oauth/introspect" if introspection_enabled
88
+
89
+ apply_overrides!(doc, overrides)
90
+ end
91
+
92
+ # Merge resolved overrides into the document.
93
+ #
94
+ # A `nil` value REMOVES the member rather than emitting `null`. Real apps
95
+ # need that — one omits `scopes_supported` entirely — and a `null` would be
96
+ # worse than either alternative, since RFC 8414 members are typed.
97
+ #
98
+ # `issuer` is REFUSED rather than silently dropped. It is a security
99
+ # identifier clients match byte-for-byte against both their discovery URL
100
+ # and the `iss` claim; letting a metadata override diverge it from what the
101
+ # token service actually stamps would produce a document that validates
102
+ # against nothing, and the failure would surface a long way from here.
103
+ def apply_overrides!(doc, overrides)
104
+ return doc if overrides.blank?
105
+
106
+ overrides = overrides.symbolize_keys
107
+ if overrides.key?(:issuer)
108
+ raise StandardId::ConfigurationError,
109
+ "discovery_metadata_overrides cannot set `issuer`. The issuer is a stable " \
110
+ "security identifier (RFC 8414 §2): clients compare it byte-for-byte with " \
111
+ "the URL they used for discovery and with the `iss` claim of issued tokens, " \
112
+ "so it must stay exactly StandardId.config.issuer. To move where the " \
113
+ "ENDPOINTS live, set config.oauth.discovery_endpoint_base instead."
114
+ end
115
+
116
+ overrides.each do |key, value|
117
+ value.nil? ? doc.delete(key) : doc[key] = value
118
+ end
58
119
 
59
120
  doc
60
121
  end
@@ -0,0 +1,158 @@
1
+ module StandardId
2
+ module Oauth
3
+ # Resolves the three request-dependent inputs of a discovery document:
4
+ # the issuer, the endpoint base, and the overrides.
5
+ #
6
+ # Lives apart from DiscoveryDocument so the builder stays a pure function of
7
+ # its arguments (and stays trivially testable without a request), while all
8
+ # the "where am I actually mounted" reasoning is in one place.
9
+ module DiscoveryResolver
10
+ # Values of `discovery_endpoint_base` that mean "derive it from the
11
+ # request". Compared as strings so `:request` and `"request"` both work —
12
+ # this is set from an initializer and the symbol/string distinction is not
13
+ # something a config file should have to get right.
14
+ REQUEST_DERIVED = %w[request].freeze
15
+
16
+ module_function
17
+
18
+ # The issuer, verbatim from config. Never derived from the request, never
19
+ # overridable: RFC 8414 §2 makes it a stable security identifier that
20
+ # clients match byte-for-byte against their discovery URL and against the
21
+ # `iss` claim of issued tokens.
22
+ #
23
+ # @return [String, nil]
24
+ def issuer
25
+ StandardId.config.issuer
26
+ end
27
+
28
+ # Base URL the advertised endpoints hang off, from
29
+ # `config.oauth.discovery_endpoint_base`:
30
+ #
31
+ # nil (default) — the issuer. EXACTLY the behaviour before this option
32
+ # existed, so an upgrade changes no document. Correct
33
+ # already for an app whose issuer carries the mount path
34
+ # (`https://host/auth/api`); wrong, and the bug this
35
+ # option exists for, when it does not.
36
+ # :request — `request.base_url` + the detected mount path. This is
37
+ # what an app mounting ApiEngine under a prefix wants.
38
+ # String — used verbatim.
39
+ # callable — called with `(request:)`. The escape hatch for a proxy
40
+ # that rewrites `request.base_url`, and for an app that
41
+ # mounts ApiEngine more than once (under host constraints,
42
+ # say) where no single detected path is right.
43
+ #
44
+ # Request-derived is deliberately OPT-IN rather than the default. Making it
45
+ # the default would silently rewrite the document of every app whose issuer
46
+ # host differs from the host serving the request — which is exactly the
47
+ # split-host setup a separate issuer exists to express.
48
+ #
49
+ # @param request [ActionDispatch::Request, nil]
50
+ # @return [String, nil]
51
+ def endpoint_base(request: nil)
52
+ configured = StandardId.config.oauth.discovery_endpoint_base
53
+
54
+ return issuer if configured.blank?
55
+
56
+ if configured.respond_to?(:call)
57
+ resolved = configured.call(request: request)
58
+ return resolved.present? ? resolved.to_s.chomp("/") : issuer
59
+ end
60
+
61
+ return configured.to_s.chomp("/") unless REQUEST_DERIVED.include?(configured.to_s)
62
+ return issuer if request.nil?
63
+
64
+ "#{request.base_url}#{mount_path(request)}".chomp("/")
65
+ end
66
+
67
+ # The path ApiEngine is mounted at, as seen from this request.
68
+ #
69
+ # Two cases, and the difference is why this is not a config lookup:
70
+ #
71
+ # * The document is served from INSIDE the engine mount (the gem's own
72
+ # `scope ".well-known"` routes). Rails sets SCRIPT_NAME to the mount
73
+ # prefix, so `request.script_name` IS the mount path, exactly, with no
74
+ # detection or guessing. This is the case that used to produce 404-ing
75
+ # endpoint URLs.
76
+ # * The document is served from a ROOT route drawn by
77
+ # `standard_id_well_known_routes` (RFC 8615 clients probe the origin
78
+ # root, which is outside any engine mount). There `script_name` is
79
+ # empty, so the helper stamps the mount path it was given into the
80
+ # route defaults and we read it back from `params`.
81
+ #
82
+ # @return [String] "" when neither is available, which resolves to the
83
+ # origin root — correct for an app that mounts ApiEngine at "/".
84
+ def mount_path(request)
85
+ from_defaults = request.params[MOUNT_PATH_PARAM].presence
86
+ return normalize_path(from_defaults) if from_defaults
87
+
88
+ normalize_path(request.script_name)
89
+ end
90
+
91
+ # Route default the routing helper uses to carry the mount path to a
92
+ # root-served document. Named rather than positional so it cannot collide
93
+ # with a host's own params.
94
+ MOUNT_PATH_PARAM = :standard_id_mount_path
95
+
96
+ # Resolve `config.oauth.discovery_metadata_overrides` for this request.
97
+ #
98
+ # Values may be static or callable. A callable receives one Hash argument
99
+ # so members can be built from the request without each app re-deriving the
100
+ # origin:
101
+ #
102
+ # {
103
+ # # host-owned shim that injects the audience before handing off to the
104
+ # # engine's /authorize
105
+ # authorization_endpoint: ->(ctx) { "#{ctx[:origin]}/oauth/authorize" },
106
+ # # deliberately narrower than what the server can mint
107
+ # scopes_supported: %w[mcp mcp:read],
108
+ # # must mirror the host's DCR policy, or spec-following clients
109
+ # # register in a way that policy then rejects
110
+ # token_endpoint_auth_methods_supported: %w[none],
111
+ # # remove a member entirely
112
+ # grant_types_supported: nil
113
+ # }
114
+ #
115
+ # Context keys: `:origin` (scheme+host+port, no path), `:endpoint_base`,
116
+ # `:issuer`, `:request`.
117
+ #
118
+ # @return [Hash] with callables evaluated
119
+ def metadata_overrides(request: nil, endpoint_base: nil)
120
+ configured = StandardId.config.oauth.discovery_metadata_overrides
121
+ return {} if configured.blank?
122
+
123
+ context = {
124
+ origin: request&.base_url,
125
+ endpoint_base: endpoint_base,
126
+ issuer: issuer,
127
+ request: request
128
+ }
129
+
130
+ configured.to_h.each_with_object({}) do |(key, value), acc|
131
+ acc[key.to_sym] = value.respond_to?(:call) ? value.call(context) : value
132
+ end
133
+ end
134
+
135
+ # Everything a well-known controller needs, in one call.
136
+ #
137
+ # @return [Hash] `{ issuer:, endpoint_base:, overrides: }`
138
+ def resolve(request: nil)
139
+ base = endpoint_base(request: request)
140
+
141
+ {
142
+ issuer: issuer,
143
+ endpoint_base: base,
144
+ overrides: metadata_overrides(request: request, endpoint_base: base)
145
+ }
146
+ end
147
+
148
+ # "" or "/foo" — never "/" and never a trailing slash, so string
149
+ # concatenation against an origin cannot produce a double slash.
150
+ def normalize_path(path)
151
+ normalized = path.to_s.chomp("/")
152
+ return "" if normalized.empty?
153
+
154
+ normalized.start_with?("/") ? normalized : "/#{normalized}"
155
+ end
156
+ end
157
+ end
158
+ end
@@ -18,11 +18,76 @@ module StandardId
18
18
  def register(name, provider_class)
19
19
  validate_provider!(provider_class)
20
20
  providers[name.to_s] = provider_class
21
- register_config_schema(provider_class)
21
+ declare_config_schema(provider_class)
22
22
  provider_class.setup if provider_class.respond_to?(:setup)
23
23
  provider_class
24
24
  end
25
25
 
26
+ # Declare the `social` config fields of every provider class that has been
27
+ # LOADED, whether or not it has been `register`ed yet.
28
+ #
29
+ # Called by a core Engine initializer that runs `before:
30
+ # :load_config_initializers` — see StandardId::Engine. It exists because
31
+ # provider plugins register themselves from their Railtie's
32
+ # `config.after_initialize`, which runs long AFTER the host's
33
+ # `config/initializers/standard_id.rb`. A host initializer writing
34
+ # `c.social.google_client_id` therefore hit `Scope#[]=` → `validate!`
35
+ # before the field existed and raised StandardId::ConfigurationError, with
36
+ # nothing in the message to suggest the cause was ordering. Every consuming
37
+ # app that used a provider plugin independently discovered the same
38
+ # `Rails.application.config.after_initialize { ... }` wrapper to work
39
+ # around it.
40
+ #
41
+ # This is safe to do early because provider classes are required at
42
+ # gem-require time (`require "standard_id/google/providers/google"` in the
43
+ # plugin's entry file), so `Providers::Base.subclasses` is already
44
+ # populated before any initializer runs.
45
+ #
46
+ # Only FIELD DECLARATION moves earlier. Full `register` — which also runs
47
+ # `validate_provider!` and the provider's `setup` — deliberately stays in
48
+ # `after_initialize`, where the host's configuration is complete and
49
+ # `setup` can rely on it.
50
+ #
51
+ # Idempotent: `ConfigSchema#add_field` uses `compute_if_absent`, so a field
52
+ # already declared here is untouched when the plugin later calls `register`.
53
+ #
54
+ # @return [Array<Class>] the provider classes whose fields were declared
55
+ def declare_config_schemas!
56
+ provider_classes.each { |provider_class| declare_config_schema(provider_class) }
57
+ end
58
+
59
+ # Provider classes known to the process: every loaded subclass of
60
+ # Providers::Base, plus anything already registered (a registered class
61
+ # need not be a direct subclass).
62
+ #
63
+ # @return [Array<Class>]
64
+ def provider_classes
65
+ (StandardId::Providers::Base.subclasses + providers.values).uniq
66
+ end
67
+
68
+ # Declare one provider's config fields against the `social` scope.
69
+ #
70
+ # Thread-safe and idempotent — adding the same field twice is a no-op.
71
+ #
72
+ # `add_field` is retroactive: `ConfigSchema::Scope#validate!` and `#[]`
73
+ # both consult the schema live (the latter falling back to
74
+ # `field_for(...).default_value` for an unwritten key), so declaring a
75
+ # field after `StandardId.config` has been built works exactly as if it
76
+ # had been declared before. That is what makes the existing host-side
77
+ # `after_initialize` wrappers keep working untouched.
78
+ #
79
+ # @param provider_class [Class] Provider implementation class
80
+ def declare_config_schema(provider_class)
81
+ return unless provider_class.respond_to?(:config_schema)
82
+
83
+ schema = provider_class.config_schema
84
+ return if schema.nil? || schema.empty?
85
+
86
+ schema.each do |field_name, options|
87
+ StandardId::ConfigSchema.add_field(scope: :social, name: field_name, **options)
88
+ end
89
+ end
90
+
26
91
  # Get provider by name
27
92
  # @param name [Symbol, String] Provider identifier
28
93
  # @return [Class] Provider class
@@ -49,18 +114,6 @@ module StandardId
49
114
 
50
115
  private
51
116
 
52
- # Register provider's config schema fields with the StandardId schema.
53
- # Thread-safe and idempotent — adding the same field twice is a no-op.
54
- # @param provider_class [Class] Provider implementation class
55
- def register_config_schema(provider_class)
56
- schema = provider_class.config_schema
57
- return if schema.nil? || schema.empty?
58
-
59
- schema.each do |field_name, options|
60
- StandardId::ConfigSchema.add_field(scope: :social, name: field_name, **options)
61
- end
62
- end
63
-
64
117
  def validate_provider!(provider_class)
65
118
  unless provider_class.is_a?(Class)
66
119
  raise InvalidProviderError,
@@ -0,0 +1,133 @@
1
+ require "action_dispatch/routing/mapper"
2
+
3
+ module StandardId
4
+ # Route-mapper extensions, available inside `Rails.application.routes.draw`.
5
+ module Routing
6
+ # Documents this helper can draw, mapped to their path and controller.
7
+ WELL_KNOWN_DOCUMENTS = {
8
+ oauth_authorization_server: {
9
+ path: "oauth-authorization-server",
10
+ to: "standard_id/api/well_known/oauth_authorization_server#show"
11
+ },
12
+ openid_configuration: {
13
+ path: "openid-configuration",
14
+ to: "standard_id/api/well_known/openid_configuration#show"
15
+ },
16
+ jwks: {
17
+ path: "jwks.json",
18
+ to: "standard_id/api/well_known/jwks#show"
19
+ }
20
+ }.freeze
21
+
22
+ # Draw StandardId's discovery documents at the ORIGIN ROOT.
23
+ #
24
+ # # config/routes.rb
25
+ # Rails.application.routes.draw do
26
+ # standard_id_well_known_routes at: "/api/v1"
27
+ # namespace :api do
28
+ # mount StandardId::ApiEngine, at: "/", as: :standard_id_api
29
+ # end
30
+ # end
31
+ #
32
+ # ## Why the gem cannot just route these itself
33
+ #
34
+ # The engine's own `.well-known` routes live INSIDE its mount, so an app
35
+ # mounting ApiEngine at `/api/v1` serves them at
36
+ # `/api/v1/.well-known/openid-configuration`. But RFC 8615 clients probe the
37
+ # **origin root**, which is outside any engine mount — a mapper helper in the
38
+ # host's routes file is the only place these can be drawn.
39
+ #
40
+ # ## The path-inserted form, and why it is not optional
41
+ #
42
+ # For each document this draws BOTH:
43
+ #
44
+ # /.well-known/oauth-authorization-server
45
+ # /.well-known/oauth-authorization-server/api/v1 <- RFC 8414 §3.1
46
+ #
47
+ # The second is the spec's form for an issuer that carries a path: the
48
+ # well-known segment is INSERTED BEFORE the issuer's path rather than appended
49
+ # to it. It looks redundant and it is not. It is the URL Claude Code actually
50
+ # requests in production against a path-carrying issuer; without it the client
51
+ # 404s, falls back to issuer-relative defaults (hitting the engine's real
52
+ # `/authorize` instead of a host audience shim), and every subsequent
53
+ # authenticated request 401s. That failure is remote from its cause and hard
54
+ # to diagnose, which is why the route is drawn for you rather than documented
55
+ # as an extra step.
56
+ #
57
+ # Both forms serve the same document. The mount path is stamped into the
58
+ # route defaults so DiscoveryResolver can build endpoint URLs off it — a root
59
+ # route has no SCRIPT_NAME to read it from.
60
+ #
61
+ # @param at [String] the path ApiEngine is mounted at ("/" or "" for root).
62
+ # Used both to build the path-inserted routes and to resolve endpoint URLs.
63
+ # @param only [Array<Symbol>, Symbol, nil] restrict to these documents.
64
+ # Keys: :oauth_authorization_server, :openid_configuration, :jwks. Apps
65
+ # that keep a host-owned metadata controller but want the gem's JWKS at the
66
+ # root pass `only: :jwks`.
67
+ # @param except [Array<Symbol>, Symbol, nil] the inverse of `only`.
68
+ # @param extra_paths [Array<String>] additional path-inserted suffixes beyond
69
+ # `at:`. For serving the same document under a second issuer path (e.g. a
70
+ # protected resource at `/mcp`) without a second route block.
71
+ # @param path_inserted [Boolean] set false to draw only the bare root forms.
72
+ # Defaults to true; turning it off is what reintroduces the Claude Code
73
+ # failure above, so it exists only for apps that route those themselves.
74
+ def standard_id_well_known_routes(at:, only: nil, except: nil, extra_paths: [], path_inserted: true)
75
+ mount_path = StandardId::Oauth::DiscoveryResolver.normalize_path(at)
76
+ documents = StandardId::Routing.selected_documents(only: only, except: except)
77
+
78
+ suffixes = path_inserted ? ([mount_path] + Array(extra_paths)).map { |p| StandardId::Oauth::DiscoveryResolver.normalize_path(p) } : []
79
+ suffixes = suffixes.reject(&:empty?).uniq
80
+
81
+ defaults = { StandardId::Oauth::DiscoveryResolver::MOUNT_PATH_PARAM => mount_path }
82
+
83
+ scope ".well-known" do
84
+ documents.each do |name, config|
85
+ get config[:path], to: config[:to], as: :"standard_id_root_#{name}", defaults: defaults
86
+
87
+ # jwks.json is a concrete file path, not a metadata document whose
88
+ # location RFC 8414 §3.1 relocates — a path-inserted variant of it
89
+ # would advertise a URL no spec describes.
90
+ next if name == :jwks
91
+
92
+ suffixes.each_with_index do |suffix, index|
93
+ get "#{config[:path]}#{suffix}",
94
+ to: config[:to],
95
+ as: :"standard_id_root_#{name}_at_#{index}",
96
+ defaults: defaults
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ # @return [Hash] the WELL_KNOWN_DOCUMENTS entries `only:`/`except:` select
103
+ def self.selected_documents(only: nil, except: nil)
104
+ if only.present? && except.present?
105
+ raise ArgumentError, "standard_id_well_known_routes accepts `only:` or `except:`, not both"
106
+ end
107
+
108
+ keys = WELL_KNOWN_DOCUMENTS.keys
109
+ requested = Array(only).map(&:to_sym) + Array(except).map(&:to_sym)
110
+ unknown = requested - keys
111
+ if unknown.any?
112
+ raise ArgumentError,
113
+ "Unknown StandardId well-known document(s): #{unknown.join(', ')}. Valid: #{keys.join(', ')}"
114
+ end
115
+
116
+ selected = if only.present?
117
+ Array(only).map(&:to_sym)
118
+ elsif except.present?
119
+ keys - Array(except).map(&:to_sym)
120
+ else
121
+ keys
122
+ end
123
+
124
+ WELL_KNOWN_DOCUMENTS.slice(*selected)
125
+ end
126
+ end
127
+ end
128
+
129
+ # Included directly rather than through a load hook: ActionDispatch has no
130
+ # `:action_dispatch_routing_mapper` hook, and this file is required from
131
+ # lib/standard_id.rb during Bundler.require — long before any routes file is
132
+ # evaluated — so the mapper method is defined by the time a host calls it.
133
+ ActionDispatch::Routing::Mapper.include(StandardId::Routing)
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.32.0"
2
+ VERSION = "0.34.0"
3
3
  end
data/lib/standard_id.rb CHANGED
@@ -6,6 +6,7 @@ require "standard_id/api_engine"
6
6
  require "standard_id/config_schema"
7
7
  require "standard_id/config/schema"
8
8
  require "standard_id/scope_config"
9
+ require "standard_id/association_strict_loading"
9
10
  require "standard_id/errors"
10
11
  require "standard_id/events"
11
12
  require "standard_id/events/subscribers/base"
@@ -44,7 +45,9 @@ require "standard_id/oauth/subflows/base"
44
45
  require "standard_id/oauth/subflows/traditional_code_grant"
45
46
  require "standard_id/oauth/subflows/social_login_grant"
46
47
  require "standard_id/oauth/passwordless_otp_flow"
48
+ require "standard_id/oauth/discovery_resolver"
47
49
  require "standard_id/oauth/discovery_document"
50
+ require "standard_id/routing"
48
51
  require "standard_id/oauth/consent_payload"
49
52
  require "standard_id/oauth/client_registration"
50
53
  require "standard_id/passwordless/base_strategy"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.32.0
4
+ version: 0.34.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim
@@ -133,6 +133,7 @@ files:
133
133
  - app/controllers/standard_id/api/base_controller.rb
134
134
  - app/controllers/standard_id/api/oauth/base_controller.rb
135
135
  - app/controllers/standard_id/api/oauth/callback/providers_controller.rb
136
+ - app/controllers/standard_id/api/oauth/introspections_controller.rb
136
137
  - app/controllers/standard_id/api/oauth/registrations_controller.rb
137
138
  - app/controllers/standard_id/api/oauth/revocations_controller.rb
138
139
  - app/controllers/standard_id/api/oauth/tokens_controller.rb
@@ -238,6 +239,7 @@ files:
238
239
  - lib/standard_id/api/session_manager.rb
239
240
  - lib/standard_id/api/token_manager.rb
240
241
  - lib/standard_id/api_engine.rb
242
+ - lib/standard_id/association_strict_loading.rb
241
243
  - lib/standard_id/authorization_bypass.rb
242
244
  - lib/standard_id/bearer_token_extraction.rb
243
245
  - lib/standard_id/config/callable_validator.rb
@@ -267,6 +269,7 @@ files:
267
269
  - lib/standard_id/oauth/client_registration.rb
268
270
  - lib/standard_id/oauth/consent_payload.rb
269
271
  - lib/standard_id/oauth/discovery_document.rb
272
+ - lib/standard_id/oauth/discovery_resolver.rb
270
273
  - lib/standard_id/oauth/implicit_authorization_flow.rb
271
274
  - lib/standard_id/oauth/oauth_session_persistence.rb
272
275
  - lib/standard_id/oauth/password_flow.rb
@@ -287,6 +290,7 @@ files:
287
290
  - lib/standard_id/provider_registry.rb
288
291
  - lib/standard_id/providers/base.rb
289
292
  - lib/standard_id/rate_limit_store.rb
293
+ - lib/standard_id/routing.rb
290
294
  - lib/standard_id/scope_config.rb
291
295
  - lib/standard_id/session_type_resolver.rb
292
296
  - lib/standard_id/testing.rb