doorkeeper-openid_connect 1.10.4 → 1.10.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c7b483ae4502881dca6aece41d05d538677d0e8d53f2fa3b82d9b89dc9aa1d8
4
- data.tar.gz: dd8ed48877f8c68199c466afba4bd27a3b26671a00bd5af949146e103849544e
3
+ metadata.gz: cc9243cc386621bb9b9474617c885f398632d60fdeabc7ecb50deab1364bb787
4
+ data.tar.gz: e33f32f4992a35ec43762c4c5c60d0ba6e98bb8b4f7c60c8cd0d4d69b4c1f535
5
5
  SHA512:
6
- metadata.gz: 18b20b9a1d04b2dfe1bfd6a169057e08fee82d538990e506f46df58f70ff29a8681d30055e18542f2af82fdb3ebceb11dc976d8b4d55f2b0f01e41fb406b931c
7
- data.tar.gz: 86b69e55903f371666178cfcaa8989e4d0c82939172a79bb070d3e50742ad4cac885edfd498d5c53af18cc98643c25cb63dc094b0c55f91c1279aec446f039a0
6
+ metadata.gz: ca8e15dde2fb57d5badf44ddb67f5109eb7b419e92ae33b4f7c3bee6cc998ae4ec50a31bc57aefae40578c5c094a85ac5cfc8cabdbbd4a1c77316fcc7651faf4
7
+ data.tar.gz: 7ac0048a88dc1968427dbb67ab814e85ebc361d3210d2d6d6bf29e52bd4f09fd94f6eb9792ab1c495ca7ef9323a9d0ca29d43aba8c13e6658cfe97c17f2a2230
data/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  ## Unreleased
2
2
 
3
+ ## v1.10.5 (2026-07-09)
4
+
5
+ - [#329] Restore compatibility with the full `doorkeeper >= 5.5` range declared in the gemspec ([#328](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/328)). Doorkeeper 5.5.x raised `NameError: uninitialized constant Doorkeeper::Orm::ActiveRecord::Mixins` at load time (the mixin file ships since 5.5.0 but is only autoloadable since 5.6.0 — it is now required explicitly when absent), and Doorkeeper 5.6/5.7 broke the discovery endpoint because `pkce_code_challenge_methods` only exists since 5.8.0 (the discovery response now falls back to `plain S256`, which is what those versions accept). CI now exercises the oldest supported Doorkeeper series via `gemfiles/doorkeeper_5.{5,6,7}.gemfile`
3
6
  - Please add here
7
+ - [#325] Remove the dead `Claims::AggregatedClaim` and `Claims::DistributedClaim` classes. They have never been required, instantiated, or reachable through the claims DSL (`ClaimsBuilder` only builds `NormalClaim`), and the discovery document only advertises `claim_types_supported: ["normal"]` — the files just shipped in the gem unused since 2016
8
+ - [#324] Split `Helpers::Controller` (294 lines, one module) into focused submodules — `Prompt`, `MaxAge`, `ErrorResponse`, `TokenMatching` — and decompose the two most complex methods (`handle_oidc_prompt_param!`, `handle_oidc_max_age_param!`). This is a behavior-preserving refactor that allows dropping the `Metrics/ModuleLength`, `Metrics/CyclomaticComplexity` and `Metrics/PerceivedComplexity` overrides from `.rubocop_todo.yml`; the codebase now passes those cops at RuboCop's default thresholds
9
+ - [#326] Make the dynamic client registration spec robust against pre-existing `Doorkeeper::Application` rows: replace absolute `Application.count` assertions and `Application.first` lookups with relative `change` matchers and `Application.find_by(uid:)` lookups keyed on the `client_id` returned by each request. With a polluted `test.sqlite3` (e.g. left behind by a `rake server` session against the same dummy app) 12 of the 20 examples failed spuriously; they now pass regardless of prior DB state
10
+ - [#327] Document previously-undocumented config options in the generated initializer template: `authorize_dynamic_client_registration` (DCR endpoint authorization gate), `apply_prompt_to_non_oidc_requests`, `end_session_endpoint`, `discovery_url_options`, and the `response:` option for claims (controlling whether a claim appears in the ID Token, UserInfo, or both). No behavior change
4
11
 
5
12
  ## v1.10.4 (2026-07-07)
6
13
 
data/README.md CHANGED
@@ -66,7 +66,7 @@ rails generate doorkeeper:openid_connect:migration
66
66
  rake db:migrate
67
67
  ```
68
68
 
69
- If you're upgrading from an earlier version, check [Migration from old versions](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Migration%E2%80%90from%E2%80%90old%E2%80%90versions)
69
+ If you're upgrading from an earlier version, check [Migration from Old Versions](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Migration-from-Old-Versions)
70
70
  wiki and [CHANGELOG.md](CHANGELOG.md) for upgrade instructions.
71
71
 
72
72
  ## Configuration
@@ -5,6 +5,11 @@ module Doorkeeper
5
5
  module AuthorizationsExtension
6
6
  private
7
7
 
8
+ # Whitelist the OIDC `nonce` authorization parameter so Doorkeeper
9
+ # carries it into the PreAuthorization (see
10
+ # Doorkeeper::OpenidConnect::OAuth::PreAuthorization#initialize, which
11
+ # reads `attrs[:nonce]`). Without this the nonce would be dropped and
12
+ # never make it onto the issued ID Token.
8
13
  def pre_auth_param_fields
9
14
  super.append(:nonce)
10
15
  end
@@ -83,6 +83,10 @@ module Doorkeeper
83
83
  def code_challenge_methods_supported(doorkeeper)
84
84
  return unless doorkeeper.access_grant_model.pkce_supported?
85
85
 
86
+ # Doorkeeper < 5.8 has no `pkce_code_challenge_methods` option and
87
+ # always accepts both methods whenever PKCE is available.
88
+ return %w[plain S256] unless doorkeeper.respond_to?(:pkce_code_challenge_methods)
89
+
86
90
  doorkeeper.pkce_code_challenge_methods
87
91
  end
88
92
 
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module OpenidConnect
5
+ module Helpers
6
+ module Controller
7
+ # Renders OpenID Connect authorization errors (`login_required`,
8
+ # `consent_required`, ...) through Doorkeeper's OAuth response objects.
9
+ module ErrorResponse
10
+ private
11
+
12
+ def handle_oidc_error!(exception)
13
+ clear_oidc_response
14
+
15
+ error_response = if exception.type == :invalid_request
16
+ ::Doorkeeper::OAuth::InvalidRequestResponse.new(
17
+ name: exception.type,
18
+ state: params[:state],
19
+ redirect_uri: params[:redirect_uri],
20
+ response_on_fragment: pre_auth.response_on_fragment?,
21
+ )
22
+ else
23
+ ::Doorkeeper::OAuth::ErrorResponse.new(
24
+ name: exception.type,
25
+ state: params[:state],
26
+ redirect_uri: params[:redirect_uri],
27
+ response_on_fragment: pre_auth.response_on_fragment?,
28
+ )
29
+ end
30
+
31
+ response.headers.merge!(error_response.headers)
32
+
33
+ # NOTE: Assign error_response to @authorize_response then use the
34
+ # redirect_or_render method defined by doorkeeper's
35
+ # authorizations_controller.
36
+ # - https://github.com/doorkeeper-gem/doorkeeper/blob/v5.5.0/app/controllers/doorkeeper/authorizations_controller.rb#L110
37
+ # - https://github.com/doorkeeper-gem/doorkeeper/blob/v5.5.0/app/controllers/doorkeeper/authorizations_controller.rb#L52
38
+ @authorize_response = error_response
39
+ redirect_or_render(@authorize_response)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module OpenidConnect
5
+ module Helpers
6
+ module Controller
7
+ # Enforces the OIDC `max_age` authorization parameter (OIDC Core 1.0
8
+ # §3.1.2.1): when the resource owner's last authentication is older
9
+ # than `max_age` seconds, reauthentication is required.
10
+ module MaxAge
11
+ private
12
+
13
+ def handle_oidc_max_age_param!(owner)
14
+ max_age = params[:max_age].to_i
15
+ return unless (params[:max_age].to_s == "0" || max_age > 0) && owner
16
+
17
+ auth_time = normalized_oidc_auth_time(owner)
18
+
19
+ # NOTE: clock skew
20
+ max_age = [1, max_age].max
21
+
22
+ return unless oidc_auth_time_stale?(auth_time, max_age)
23
+
24
+ # OIDC Core 1.0 §3.1.2.1: with `prompt=none` the Authorization Server
25
+ # MUST NOT display any authentication UI. Reauthentication required by
26
+ # `max_age` must therefore be reported as `login_required` instead of
27
+ # triggering the interactive `reauthenticate_resource_owner` flow.
28
+ # (Conflicting combinations like `prompt=none login` are still left to
29
+ # `handle_oidc_prompt_param!`, which raises `invalid_request`.)
30
+ raise Errors::LoginRequired if oidc_prompt_values == ["none"]
31
+
32
+ reauthenticate_oidc_resource_owner(owner)
33
+ end
34
+
35
+ # Normalize non-Time values (e.g. an Integer epoch) so that the
36
+ # staleness subtraction yields a Float of elapsed seconds rather
37
+ # than a shifted Time value.
38
+ def normalized_oidc_auth_time(owner)
39
+ auth_time = resolve_oidc_auth_time(owner)
40
+ return auth_time if !auth_time || auth_time.is_a?(Time) || auth_time.is_a?(DateTime)
41
+
42
+ Time.zone.at(auth_time.to_i)
43
+ end
44
+
45
+ def oidc_auth_time_stale?(auth_time, max_age)
46
+ !auth_time || (Time.zone.now - auth_time) > max_age
47
+ end
48
+
49
+ # Resolve auth_time for max_age enforcement.
50
+ #
51
+ # Prefers `auth_time_from_session` so that multi-session deployments can
52
+ # return the auth_time of the *current* session rather than the user's
53
+ # most recent login on any device (issue #150). Falls back to the legacy
54
+ # `auth_time_from_resource_owner` with a one-time deprecation warning.
55
+ def resolve_oidc_auth_time(owner)
56
+ config = Doorkeeper::OpenidConnect.configuration
57
+
58
+ if config.auth_time_from_session
59
+ return instance_exec(session, request, &config.auth_time_from_session)
60
+ end
61
+
62
+ Doorkeeper::OpenidConnect::Helpers::Controller.warn_auth_time_from_resource_owner_deprecation
63
+ instance_exec(owner, &config.auth_time_from_resource_owner)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module OpenidConnect
5
+ module Helpers
6
+ module Controller
7
+ # Handles the OIDC `prompt` authorization parameter (OIDC Core 1.0
8
+ # §3.1.2.1): `none`, `login`, `consent`, `select_account`.
9
+ module Prompt
10
+ private
11
+
12
+ def handle_oidc_prompt_param!(owner)
13
+ priority = %w[none consent login select_account]
14
+ prompt_values = oidc_prompt_values.sort_by do |prompt|
15
+ priority.find_index(prompt).to_i
16
+ end
17
+
18
+ prompt_values.each do |prompt|
19
+ apply_oidc_prompt!(prompt, prompt_values, owner)
20
+ end
21
+ end
22
+
23
+ def apply_oidc_prompt!(prompt, prompt_values, owner)
24
+ case prompt
25
+ when "none"
26
+ handle_oidc_prompt_none!(prompt_values, owner)
27
+ when "login"
28
+ handle_oidc_prompt_login!(owner)
29
+ when "consent"
30
+ handle_oidc_prompt_consent!(owner)
31
+ when "select_account"
32
+ select_account_for_oidc_resource_owner(owner)
33
+ when "create"
34
+ # NOTE: not supported, but does not raise an error.
35
+ else
36
+ raise Errors::InvalidRequest
37
+ end
38
+ end
39
+
40
+ def handle_oidc_prompt_none!(prompt_values, owner)
41
+ raise Errors::InvalidRequest if (prompt_values - ["none"]).any?
42
+ raise Errors::LoginRequired unless owner
43
+ raise Errors::ConsentRequired if oidc_consent_required?(owner)
44
+
45
+ # Issue #63: if an active token already covers the requested scopes
46
+ # (a non-strict superset, including the exact-match case), force
47
+ # auto-issue rather than rendering the consent form — `prompt=none`
48
+ # forbids any interactive UI (OIDC Core §3.1.2.1).
49
+ @_oidc_prompt_none_skip_authorization = true if oidc_matching_subset_token?(owner)
50
+ end
51
+
52
+ def handle_oidc_prompt_login!(owner)
53
+ reauthenticate_oidc_resource_owner(owner) if owner
54
+ end
55
+
56
+ def handle_oidc_prompt_consent!(owner)
57
+ return unless owner
58
+
59
+ clear_oidc_response
60
+ render :new
61
+ end
62
+
63
+ def oidc_prompt_values
64
+ # Reject blank entries so leading/duplicate spaces in the
65
+ # space-delimited `prompt` parameter don't surface as an empty
66
+ # value (which would otherwise be treated as an unknown prompt and
67
+ # rejected with `invalid_request`).
68
+ @oidc_prompt_values ||= params[:prompt].to_s.split(/ +/).reject(&:blank?).uniq
69
+ end
70
+
71
+ def return_without_oidc_prompt_param(prompt_value)
72
+ return_to = URI.parse(request.path)
73
+ return_to.query = request.query_parameters.tap do |params|
74
+ params["prompt"] = params["prompt"].to_s.sub(/\b#{prompt_value}\s*\b/, "").strip
75
+ params.delete("prompt") if params["prompt"].blank?
76
+ end.to_query
77
+ return_to.to_s
78
+ end
79
+
80
+ def reauthenticate_oidc_resource_owner(owner)
81
+ clear_oidc_response
82
+ return_to = return_without_oidc_prompt_param("login")
83
+
84
+ instance_exec(
85
+ owner,
86
+ return_to,
87
+ &Doorkeeper::OpenidConnect.configuration.reauthenticate_resource_owner
88
+ )
89
+
90
+ raise Errors::LoginRequired unless performed?
91
+ end
92
+
93
+ def select_account_for_oidc_resource_owner(owner)
94
+ clear_oidc_response
95
+ return_to = return_without_oidc_prompt_param("select_account")
96
+
97
+ instance_exec(
98
+ owner,
99
+ return_to,
100
+ &Doorkeeper::OpenidConnect.configuration.select_account_for_resource_owner
101
+ )
102
+
103
+ # OIDC Core 1.0 §3.1.2.6: if the account selection cannot be performed
104
+ # the request MUST fail with `account_selection_required` rather than
105
+ # silently continuing the authorization (mirrors the `login_required`
106
+ # backstop in #reauthenticate_oidc_resource_owner).
107
+ raise Errors::AccountSelectionRequired unless performed?
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module OpenidConnect
5
+ module Helpers
6
+ module Controller
7
+ # Decides whether an already-issued access token can satisfy the
8
+ # current authorization request, so `prompt=none` can succeed (or
9
+ # consent can be skipped) without interactive UI.
10
+ module TokenMatching
11
+ private
12
+
13
+ def oidc_consent_required?(owner)
14
+ !skip_authorization? && !matching_token? && !oidc_matching_subset_token?(owner)
15
+ end
16
+
17
+ # Returns true if the resource owner already has an active token for this
18
+ # client whose scopes are a (non-strict) superset of the requested scopes.
19
+ # Used to allow `prompt=none` to succeed when the client re-authorizes
20
+ # with a narrower set of scopes (issue #63).
21
+ #
22
+ # Scans tokens via `find_access_token_in_batches` (same pattern as
23
+ # upstream Doorkeeper's `find_matching_token`) so that installations
24
+ # with many active tokens per (client, resource owner) pair do not
25
+ # load the entire relation into memory.
26
+ def oidc_matching_subset_token?(owner)
27
+ return @oidc_matching_subset_token if defined?(@oidc_matching_subset_token)
28
+
29
+ @oidc_matching_subset_token =
30
+ !pre_auth.scopes.empty? && oidc_subset_token_exists?(owner)
31
+ end
32
+
33
+ def oidc_subset_token_exists?(owner)
34
+ token_model = Doorkeeper.config.access_token_model
35
+ relation = token_model.authorized_tokens_for(pre_auth.client.id, owner)
36
+ batch_size = Doorkeeper.configuration.token_lookup_batch_size
37
+
38
+ match_found = false
39
+ token_model.find_access_token_in_batches(relation, batch_size: batch_size) do |batch|
40
+ if batch.any? { |token| token.scopes.scopes?(pre_auth.scopes) }
41
+ match_found = true
42
+ break
43
+ end
44
+ end
45
+ match_found
46
+ end
47
+
48
+ # Force Doorkeeper's `render_success` onto the auto-issue path when a
49
+ # `prompt=none` subset-scope reauthorization has been validated above.
50
+ def skip_authorization?
51
+ return true if @_oidc_prompt_none_skip_authorization
52
+
53
+ super
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "doorkeeper/openid_connect/helpers/controller/error_response"
4
+ require "doorkeeper/openid_connect/helpers/controller/max_age"
5
+ require "doorkeeper/openid_connect/helpers/controller/prompt"
6
+ require "doorkeeper/openid_connect/helpers/controller/token_matching"
7
+
3
8
  module Doorkeeper
4
9
  module OpenidConnect
5
10
  module Helpers
@@ -27,6 +32,15 @@ module Doorkeeper
27
32
  @auth_time_from_resource_owner_deprecation_warned = false
28
33
  end
29
34
 
35
+ # Because this module is prepended onto Doorkeeper's
36
+ # Helpers::Controller, the included modules end up between it and
37
+ # Doorkeeper's implementation in the ancestor chain — their `super`
38
+ # calls (e.g. in `skip_authorization?`) still reach Doorkeeper.
39
+ include ErrorResponse
40
+ include MaxAge
41
+ include Prompt
42
+ include TokenMatching
43
+
30
44
  private
31
45
 
32
46
  # FIXME: remove after Doorkeeper will merge it
@@ -74,217 +88,14 @@ module Doorkeeper
74
88
  pre_auth.valid?
75
89
  end
76
90
 
77
- def handle_oidc_error!(exception)
78
- # clear the previous response body to avoid a DoubleRenderError
91
+ # Clear the previous response body to avoid a DoubleRenderError when
92
+ # rendering or redirecting again after the authenticator already
93
+ # produced a response.
94
+ def clear_oidc_response
79
95
  self.response_body = nil
80
96
 
81
97
  # FIXME: workaround for Rails 5, see https://github.com/rails/rails/issues/25106
82
98
  @_response_body = nil
83
-
84
- error_response = if exception.type == :invalid_request
85
- ::Doorkeeper::OAuth::InvalidRequestResponse.new(
86
- name: exception.type,
87
- state: params[:state],
88
- redirect_uri: params[:redirect_uri],
89
- response_on_fragment: pre_auth.response_on_fragment?,
90
- )
91
- else
92
- ::Doorkeeper::OAuth::ErrorResponse.new(
93
- name: exception.type,
94
- state: params[:state],
95
- redirect_uri: params[:redirect_uri],
96
- response_on_fragment: pre_auth.response_on_fragment?,
97
- )
98
- end
99
-
100
- response.headers.merge!(error_response.headers)
101
-
102
- # NOTE: Assign error_response to @authorize_response then use redirect_or_render method that are defined at
103
- # doorkeeper's authorizations_controller.
104
- # - https://github.com/doorkeeper-gem/doorkeeper/blob/v5.5.0/app/controllers/doorkeeper/authorizations_controller.rb#L110
105
- # - https://github.com/doorkeeper-gem/doorkeeper/blob/v5.5.0/app/controllers/doorkeeper/authorizations_controller.rb#L52
106
- @authorize_response = error_response
107
- redirect_or_render(@authorize_response)
108
- end
109
-
110
- def handle_oidc_prompt_param!(owner)
111
- priority = %w[none consent login select_account]
112
- prompt_values = oidc_prompt_values.sort_by do |prompt|
113
- priority.find_index(prompt).to_i
114
- end
115
-
116
- prompt_values.each do |prompt|
117
- case prompt
118
- when "none"
119
- handle_oidc_prompt_none!(prompt_values, owner)
120
- when "login"
121
- reauthenticate_oidc_resource_owner(owner) if owner
122
- when "consent"
123
- if owner
124
- clear_oidc_response
125
- render :new
126
- end
127
- when "select_account"
128
- select_account_for_oidc_resource_owner(owner)
129
- when "create"
130
- # NOTE: not supported, but not raise error.
131
- else
132
- raise Errors::InvalidRequest
133
- end
134
- end
135
- end
136
-
137
- def handle_oidc_prompt_none!(prompt_values, owner)
138
- raise Errors::InvalidRequest if (prompt_values - ["none"]).any?
139
- raise Errors::LoginRequired unless owner
140
- raise Errors::ConsentRequired if oidc_consent_required?(owner)
141
-
142
- # Issue #63: if an active token already covers the requested scopes
143
- # (a non-strict superset, including the exact-match case), force
144
- # auto-issue rather than rendering the consent form — `prompt=none`
145
- # forbids any interactive UI (OIDC Core §3.1.2.1).
146
- @_oidc_prompt_none_skip_authorization = true if oidc_matching_subset_token?(owner)
147
- end
148
-
149
- def handle_oidc_max_age_param!(owner)
150
- max_age = params[:max_age].to_i
151
- return unless (params[:max_age].to_s == "0" || max_age > 0) && owner
152
-
153
- auth_time = resolve_oidc_auth_time(owner)
154
-
155
- # Normalize non-Time values (e.g. an Integer epoch) so that the
156
- # subtraction below yields a Float of elapsed seconds rather than a
157
- # shifted Time value.
158
- if auth_time && !auth_time.is_a?(Time) && !auth_time.is_a?(DateTime)
159
- auth_time = Time.zone.at(auth_time.to_i)
160
- end
161
-
162
- # NOTE: clock skew
163
- max_age = [1, max_age].max
164
-
165
- return unless !auth_time || (Time.zone.now - auth_time) > max_age
166
-
167
- # OIDC Core 1.0 §3.1.2.1: with `prompt=none` the Authorization Server
168
- # MUST NOT display any authentication UI. Reauthentication required by
169
- # `max_age` must therefore be reported as `login_required` instead of
170
- # triggering the interactive `reauthenticate_resource_owner` flow.
171
- # (Conflicting combinations like `prompt=none login` are still left to
172
- # `handle_oidc_prompt_param!`, which raises `invalid_request`.)
173
- raise Errors::LoginRequired if oidc_prompt_values == ["none"]
174
-
175
- reauthenticate_oidc_resource_owner(owner)
176
- end
177
-
178
- def oidc_prompt_values
179
- # Reject blank entries so leading/duplicate spaces in the
180
- # space-delimited `prompt` parameter don't surface as an empty
181
- # value (which would otherwise be treated as an unknown prompt and
182
- # rejected with `invalid_request`).
183
- @oidc_prompt_values ||= params[:prompt].to_s.split(/ +/).reject(&:blank?).uniq
184
- end
185
-
186
- # Resolve auth_time for max_age enforcement.
187
- #
188
- # Prefers `auth_time_from_session` so that multi-session deployments can
189
- # return the auth_time of the *current* session rather than the user's
190
- # most recent login on any device (issue #150). Falls back to the legacy
191
- # `auth_time_from_resource_owner` with a one-time deprecation warning.
192
- def resolve_oidc_auth_time(owner)
193
- config = Doorkeeper::OpenidConnect.configuration
194
-
195
- if config.auth_time_from_session
196
- return instance_exec(session, request, &config.auth_time_from_session)
197
- end
198
-
199
- Doorkeeper::OpenidConnect::Helpers::Controller.warn_auth_time_from_resource_owner_deprecation
200
- instance_exec(owner, &config.auth_time_from_resource_owner)
201
- end
202
-
203
- def return_without_oidc_prompt_param(prompt_value)
204
- return_to = URI.parse(request.path)
205
- return_to.query = request.query_parameters.tap do |params|
206
- params["prompt"] = params["prompt"].to_s.sub(/\b#{prompt_value}\s*\b/, "").strip
207
- params.delete("prompt") if params["prompt"].blank?
208
- end.to_query
209
- return_to.to_s
210
- end
211
-
212
- def clear_oidc_response
213
- self.response_body = nil
214
- @_response_body = nil
215
- end
216
-
217
- def reauthenticate_oidc_resource_owner(owner)
218
- clear_oidc_response
219
- return_to = return_without_oidc_prompt_param("login")
220
-
221
- instance_exec(
222
- owner,
223
- return_to,
224
- &Doorkeeper::OpenidConnect.configuration.reauthenticate_resource_owner
225
- )
226
-
227
- raise Errors::LoginRequired unless performed?
228
- end
229
-
230
- def oidc_consent_required?(owner)
231
- !skip_authorization? && !matching_token? && !oidc_matching_subset_token?(owner)
232
- end
233
-
234
- # Returns true if the resource owner already has an active token for this
235
- # client whose scopes are a (non-strict) superset of the requested scopes.
236
- # Used to allow `prompt=none` to succeed when the client re-authorizes
237
- # with a narrower set of scopes (issue #63).
238
- #
239
- # Scans tokens via `find_access_token_in_batches` (same pattern as
240
- # upstream Doorkeeper's `find_matching_token`) so that installations
241
- # with many active tokens per (client, resource owner) pair do not
242
- # load the entire relation into memory.
243
- def oidc_matching_subset_token?(owner)
244
- return @oidc_matching_subset_token if defined?(@oidc_matching_subset_token)
245
-
246
- @oidc_matching_subset_token =
247
- if pre_auth.scopes.empty?
248
- false
249
- else
250
- access_token_model = Doorkeeper.config.access_token_model
251
- relation = access_token_model.authorized_tokens_for(pre_auth.client.id, owner)
252
- batch_size = Doorkeeper.configuration.token_lookup_batch_size
253
-
254
- match_found = false
255
- access_token_model.find_access_token_in_batches(relation, batch_size: batch_size) do |batch|
256
- if batch.any? { |token| token.scopes.scopes?(pre_auth.scopes) }
257
- match_found = true
258
- break
259
- end
260
- end
261
- match_found
262
- end
263
- end
264
-
265
- # Force Doorkeeper's `render_success` onto the auto-issue path when a
266
- # `prompt=none` subset-scope reauthorization has been validated above.
267
- def skip_authorization?
268
- return true if @_oidc_prompt_none_skip_authorization
269
-
270
- super
271
- end
272
-
273
- def select_account_for_oidc_resource_owner(owner)
274
- clear_oidc_response
275
- return_to = return_without_oidc_prompt_param("select_account")
276
-
277
- instance_exec(
278
- owner,
279
- return_to,
280
- &Doorkeeper::OpenidConnect.configuration.select_account_for_resource_owner
281
- )
282
-
283
- # OIDC Core 1.0 §3.1.2.6: if the account selection cannot be performed
284
- # the request MUST fail with `account_selection_required` rather than
285
- # silently continuing the authorization (mirrors the `login_required`
286
- # backstop in #reauthenticate_oidc_resource_owner).
287
- raise Errors::AccountSelectionRequired unless performed?
288
99
  end
289
100
  end
290
101
  end
@@ -9,6 +9,11 @@ module Doorkeeper
9
9
  def after_successful_response
10
10
  super
11
11
 
12
+ # The nonce was stashed on a one-time OpenidRequest row when the
13
+ # authorization code was issued (see OAuth::Authorization::Code).
14
+ # Read it before destroying the row so it can be bound to the ID
15
+ # Token, then delete the row so a leaked/replayed code cannot mint
16
+ # another token carrying the same nonce.
12
17
  openid_request = grant.openid_request
13
18
  nonce = openid_request&.nonce
14
19
  openid_request&.destroy!
@@ -8,6 +8,10 @@ module Doorkeeper
8
8
 
9
9
  def body
10
10
  if token.includes_scope? "openid"
11
+ # `id_token` is preset by the flows that carry a nonce (the
12
+ # authorization code and password grants). Grants without one —
13
+ # e.g. refresh_token — reach here with it unset, so build a
14
+ # nonce-less ID Token on the fly.
11
15
  id_token = self.id_token || Doorkeeper::OpenidConnect::IdToken.new(token)
12
16
 
13
17
  super
@@ -48,6 +48,13 @@ module Doorkeeper
48
48
  end
49
49
  end
50
50
 
51
+ # Doorkeeper 5.5.x ships the very same mixin file, but only 5.6.0+
52
+ # registers an autoload for `Doorkeeper::Orm::ActiveRecord::Mixins`, so
53
+ # on 5.5.x the constant has to be resolved by requiring it explicitly.
54
+ unless defined?(Orm::ActiveRecord::Mixins::AccessGrant)
55
+ require "doorkeeper/orm/active_record/mixins/access_grant"
56
+ end
57
+
51
58
  Orm::ActiveRecord::Mixins::AccessGrant.singleton_class.prepend(
52
59
  OpenidConnect::Orm::ActiveRecord::AccessGrantExtension,
53
60
  )
@@ -4,7 +4,7 @@ module Doorkeeper
4
4
  module OpenidConnect
5
5
  MAJOR = 1
6
6
  MINOR = 10
7
- TINY = 4
7
+ TINY = 5
8
8
  PRE = nil
9
9
 
10
10
  # Full version number
@@ -104,6 +104,55 @@ Doorkeeper::OpenidConnect.configure do
104
104
  # Enable dynamic client registration (default false)
105
105
  # dynamic_client_registration true
106
106
 
107
+ # Gate the dynamic client registration endpoint (RFC 7591 §3.1). Leave unset
108
+ # (default `nil`) to keep the endpoint open once `dynamic_client_registration`
109
+ # is enabled. Set a block to require authorization: it is evaluated in the
110
+ # controller scope (so it can read `request`, `params`, `request.headers`,
111
+ # etc.) and a falsy return rejects the request with `401 invalid_token`.
112
+ #
113
+ # authorize_dynamic_client_registration do
114
+ # # Example: require an Initial Access Token in the Authorization header.
115
+ # # Fail closed when the token isn't configured, so an unset env var can't
116
+ # # leave the endpoint open. Compare in constant time to avoid leaking the
117
+ # # token via timing; digesting first keeps the comparison fixed-length so
118
+ # # the token's length isn't leaked either.
119
+ # expected = ENV["DCR_INITIAL_ACCESS_TOKEN"].to_s
120
+ # next false if expected.empty?
121
+ #
122
+ # provided = request.headers["Authorization"].to_s
123
+ # ActiveSupport::SecurityUtils.secure_compare(
124
+ # Digest::SHA256.hexdigest(provided),
125
+ # Digest::SHA256.hexdigest("Bearer #{expected}"),
126
+ # )
127
+ # end
128
+
129
+ # By default the `prompt` parameter (`none`, `login`, `consent`,
130
+ # `select_account`) is only honored for OIDC requests (those carrying the
131
+ # `openid` scope). Enable this to also honor `prompt` on non-OIDC
132
+ # authorization requests. `max_age` stays OIDC-only, as it is defined by
133
+ # OIDC Core.
134
+ #
135
+ # apply_prompt_to_non_oidc_requests true
136
+
137
+ # End-session endpoint advertised in the discovery document
138
+ # (`end_session_endpoint`). The block is evaluated in the controller scope;
139
+ # return the absolute URL of your RP-initiated logout endpoint. Defaults to
140
+ # `nil`, which omits the member from the discovery document.
141
+ #
142
+ # end_session_endpoint do
143
+ # end_session_url
144
+ # end
145
+
146
+ # Per-endpoint overrides for the URLs generated in the discovery document
147
+ # (e.g. to advertise a different host or force HTTPS). The block receives the
148
+ # current `request` and returns a hash keyed by endpoint name.
149
+ #
150
+ # discovery_url_options do |request|
151
+ # {
152
+ # authorization: { protocol: request.ssl? ? :https : :http },
153
+ # }
154
+ # end
155
+
107
156
  # You can use your own model class if you need to extend (or even override) the default
108
157
  # Doorkeeper::OpenidConnect::Request model (e.g. to use a different database connection).
109
158
  #
@@ -132,5 +181,13 @@ Doorkeeper::OpenidConnect.configure do
132
181
  # normal_claim :_bar_ do |resource_owner|
133
182
  # resource_owner.bar
134
183
  # end
184
+
185
+ # # By default a claim is only returned from the UserInfo endpoint
186
+ # # (`response: [:user_info]`). Pass `response:` to control where it
187
+ # # appears — the ID Token, UserInfo, or both. `scope:` restricts the
188
+ # # claim to grants that include the given scope.
189
+ # normal_claim :_baz_, scope: :profile, response: %i[id_token user_info] do |resource_owner|
190
+ # resource_owner.baz
191
+ # end
135
192
  # end
136
193
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doorkeeper-openid_connect
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.4
4
+ version: 1.10.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Dengler
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-07-07 00:00:00.000000000 Z
13
+ date: 2026-07-09 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: doorkeeper
@@ -194,9 +194,7 @@ files:
194
194
  - lib/doorkeeper/oauth/id_token_token_request.rb
195
195
  - lib/doorkeeper/oauth/id_token_token_response.rb
196
196
  - lib/doorkeeper/openid_connect.rb
197
- - lib/doorkeeper/openid_connect/claims/aggregated_claim.rb
198
197
  - lib/doorkeeper/openid_connect/claims/claim.rb
199
- - lib/doorkeeper/openid_connect/claims/distributed_claim.rb
200
198
  - lib/doorkeeper/openid_connect/claims/normal_claim.rb
201
199
  - lib/doorkeeper/openid_connect/claims_builder.rb
202
200
  - lib/doorkeeper/openid_connect/config.rb
@@ -204,6 +202,10 @@ files:
204
202
  - lib/doorkeeper/openid_connect/errors.rb
205
203
  - lib/doorkeeper/openid_connect/grant_types_supported_mixin.rb
206
204
  - lib/doorkeeper/openid_connect/helpers/controller.rb
205
+ - lib/doorkeeper/openid_connect/helpers/controller/error_response.rb
206
+ - lib/doorkeeper/openid_connect/helpers/controller/max_age.rb
207
+ - lib/doorkeeper/openid_connect/helpers/controller/prompt.rb
208
+ - lib/doorkeeper/openid_connect/helpers/controller/token_matching.rb
207
209
  - lib/doorkeeper/openid_connect/id_token.rb
208
210
  - lib/doorkeeper/openid_connect/id_token_token.rb
209
211
  - lib/doorkeeper/openid_connect/oauth/authorization/code.rb
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Doorkeeper
4
- module OpenidConnect
5
- module Claims
6
- class AggregatedClaim < Claim
7
- attr_accessor :jwt
8
- end
9
- end
10
- end
11
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Doorkeeper
4
- module OpenidConnect
5
- module Claims
6
- class DistributedClaim < Claim
7
- attr_accessor :endpoint, :access_token
8
- end
9
- end
10
- end
11
- end