standard_id 0.31.0 → 0.33.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.
@@ -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.31.0"
2
+ VERSION = "0.33.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.31.0
4
+ version: 0.33.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