doorkeeper-openid_connect 2.0.0.beta1 → 2.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/app/controllers/doorkeeper/openid_connect/discovery_controller.rb +5 -0
- data/app/controllers/doorkeeper/openid_connect/dynamic_client_registration_controller.rb +13 -4
- data/lib/doorkeeper/oauth/id_token_token_request.rb +1 -1
- data/lib/doorkeeper/openid_connect/{hybrid_id_token_concern.rb → at_hash_concern.rb} +15 -6
- data/lib/doorkeeper/openid_connect/claims_builder.rb +9 -2
- data/lib/doorkeeper/openid_connect/config.rb +35 -37
- data/lib/doorkeeper/openid_connect/helpers/controller/error_response.rb +15 -17
- data/lib/doorkeeper/openid_connect/helpers/controller/prompt.rb +12 -14
- data/lib/doorkeeper/openid_connect/helpers/controller/token_matching.rb +2 -6
- data/lib/doorkeeper/openid_connect/id_token.rb +40 -4
- data/lib/doorkeeper/openid_connect/oauth/dynamic_registration_request.rb +27 -5
- data/lib/doorkeeper/openid_connect/rails/routes.rb +20 -15
- data/lib/doorkeeper/openid_connect/version.rb +1 -1
- data/lib/doorkeeper/openid_connect.rb +7 -11
- data/lib/generators/doorkeeper/openid_connect/templates/initializer.rb +14 -6
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6ac229d2a87bb6712986b26a47a87ef8758b581b1fa99ddd2475d0a860f9e65
|
|
4
|
+
data.tar.gz: 27300a20c1ad92391d2ecd2bbe8a635ccb97b490505f8d0c9752c09f8738d0ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1bc13a50ca14f08f218341c00ff2ab15d9b04cf1bc4d25b92d9d8617cb7a432acf86df5ee69bebec6b3e576956734db840ecec63453f5572f848c6e35976416
|
|
7
|
+
data.tar.gz: ce360e5a9ad631ff29da07b288e0fd74523293c4227d1a9f9bd9583a3e41539225d8a61e70ab3e26aa43ffe51ec9fb061328f0be6e7089f02639739f8f6c21e3
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ Upgrading? [Migration from Old Versions](https://github.com/doorkeeper-gem/doork
|
|
|
4
4
|
|
|
5
5
|
- Add entry here
|
|
6
6
|
|
|
7
|
+
## v2.0.0 (2026-09-22)
|
|
8
|
+
|
|
9
|
+
>[!IMPORTANT]
|
|
10
|
+
>
|
|
11
|
+
>- **Breaking (class overrides):** a configured `id_token_class` / `user_info_class` must now inherit from `Doorkeeper::OpenidConnect::IdToken` / `UserInfo` ([#364])
|
|
12
|
+
>- **Breaking (settings removed):** the deprecated `jws_private_key` / `jws_public_key` initializer settings are gone ([#387])
|
|
13
|
+
>- **Breaking (constant renamed):** `Doorkeeper::OpenidConnect::HybridIdTokenConcern` is now `AtHashConcern`, with no alias for the old name ([#399])
|
|
14
|
+
>
|
|
15
|
+
>**Coming from 1.10.x?** Every breaking change listed under 2.0.0.beta1 below applies as well — the `post_logout_redirect_uris` migration ([#243]), the `id_token token` response type change ([#337]), the removal of `IdTokenToken` ([#338]) and the Dynamic Client Registration defaults ([#350]). Those notes name `HybridIdTokenConcern`; read them as `AtHashConcern`, which is what 2.0.0 ships.
|
|
16
|
+
>
|
|
17
|
+
>[Migration from Old Versions](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Migration-from-Old-Versions) walks through each one.
|
|
18
|
+
|
|
19
|
+
- **Security:** Register a dynamically registered client with the server's `default_scopes` when the registration request omits `scope`, instead of persisting an empty scope set. Only deployments that enable `dynamic_client_registration` are affected.
|
|
20
|
+
- [#364] **Breaking:** Require `id_token_class` / `user_info_class` overrides to inherit from `Doorkeeper::OpenidConnect::IdToken` / `UserInfo` ([#344](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/344)). Adds an `IdToken#select_key` hook returning the signing key and its algorithm together (`IdToken::SigningKey`) for per-client, rotating or multi-tenant keys, and binds the `at_hash` digest to that algorithm (OIDC Core §3.2.2.10) rather than the global `signing_algorithm`
|
|
21
|
+
- [#387] **Breaking:** Remove the deprecated `jws_private_key` and `jws_public_key` initializer settings
|
|
22
|
+
- [#399] **Breaking:** Rename `Doorkeeper::OpenidConnect::HybridIdTokenConcern` to `AtHashConcern`. The old name described the `id_token token` response type as a hybrid flow, but OpenID Connect Core defines that response type under the Implicit Flow (§3.2) — the Hybrid Flow response types of §3.3 are not implemented by this gem. Removed without an alias: the constant only ever shipped in 2.0.0.beta1
|
|
23
|
+
|
|
7
24
|
## v2.0.0.beta1 (2026-08-20)
|
|
8
25
|
|
|
9
26
|
>[!IMPORTANT]
|
|
@@ -28,6 +28,10 @@ module Doorkeeper
|
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
|
+
# One method per metadata document: the provider metadata is a flat
|
|
32
|
+
# listing of endpoints and capabilities, and splitting it would hide the
|
|
33
|
+
# shape of the published document.
|
|
34
|
+
# rubocop:disable Metrics/AbcSize
|
|
31
35
|
def provider_response
|
|
32
36
|
doorkeeper = ::Doorkeeper.configuration
|
|
33
37
|
openid_connect = ::Doorkeeper::OpenidConnect.configuration
|
|
@@ -79,6 +83,7 @@ module Doorkeeper
|
|
|
79
83
|
code_challenge_methods_supported: code_challenge_methods_supported(doorkeeper),
|
|
80
84
|
}.compact
|
|
81
85
|
end
|
|
86
|
+
# rubocop:enable Metrics/AbcSize
|
|
82
87
|
|
|
83
88
|
def response_modes_supported(doorkeeper)
|
|
84
89
|
doorkeeper.authorization_response_flows.flat_map(&:response_mode_matches).uniq
|
|
@@ -16,10 +16,7 @@ module Doorkeeper
|
|
|
16
16
|
client = Doorkeeper.configuration.application_model.create!(application_params(registration))
|
|
17
17
|
render json: registration_response(client, registration), status: :created
|
|
18
18
|
rescue ActiveRecord::RecordInvalid => e
|
|
19
|
-
render json:
|
|
20
|
-
error: registration_error_code(e.record),
|
|
21
|
-
error_description: e.record.errors.full_messages.join(", "),
|
|
22
|
-
}, status: :bad_request
|
|
19
|
+
render json: record_invalid_response(e.record), status: :bad_request
|
|
23
20
|
end
|
|
24
21
|
|
|
25
22
|
private
|
|
@@ -49,6 +46,13 @@ module Doorkeeper
|
|
|
49
46
|
end
|
|
50
47
|
end
|
|
51
48
|
|
|
49
|
+
def record_invalid_response(record)
|
|
50
|
+
{
|
|
51
|
+
error: registration_error_code(record),
|
|
52
|
+
error_description: record.errors.full_messages.join(", "),
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
52
56
|
# RFC 7591 §3.2.2 registration error codes: `invalid_redirect_uri` for
|
|
53
57
|
# redirect URI problems, `invalid_client_metadata` for everything else.
|
|
54
58
|
def registration_error_code(record)
|
|
@@ -78,6 +82,10 @@ module Doorkeeper
|
|
|
78
82
|
Doorkeeper.config.application_model.column_names.include?("post_logout_redirect_uris")
|
|
79
83
|
end
|
|
80
84
|
|
|
85
|
+
# One method per metadata document: the registration response echoes
|
|
86
|
+
# the client metadata as RFC 7591 §3.2.1 lists it, and splitting it
|
|
87
|
+
# would hide the shape of the published document.
|
|
88
|
+
# rubocop:disable Metrics/AbcSize
|
|
81
89
|
def registration_response(doorkeeper_application, registration)
|
|
82
90
|
response = {
|
|
83
91
|
client_id: doorkeeper_application.uid,
|
|
@@ -111,6 +119,7 @@ module Doorkeeper
|
|
|
111
119
|
|
|
112
120
|
response
|
|
113
121
|
end
|
|
122
|
+
# rubocop:enable Metrics/AbcSize
|
|
114
123
|
end
|
|
115
124
|
end
|
|
116
125
|
end
|
|
@@ -11,7 +11,7 @@ module Doorkeeper
|
|
|
11
11
|
# serves the default and any custom id_token_class alike.
|
|
12
12
|
id_token_token = Doorkeeper::OpenidConnect.configuration.id_token_model
|
|
13
13
|
.new(auth.token, pre_auth.nonce)
|
|
14
|
-
.extend(Doorkeeper::OpenidConnect::
|
|
14
|
+
.extend(Doorkeeper::OpenidConnect::AtHashConcern)
|
|
15
15
|
|
|
16
16
|
IdTokenTokenResponse.new(pre_auth, auth, id_token_token)
|
|
17
17
|
end
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Doorkeeper
|
|
4
4
|
module OpenidConnect
|
|
5
|
-
# Adds the `at_hash` claim required
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
|
|
5
|
+
# Adds the `at_hash` claim required when an ID Token is issued alongside an
|
|
6
|
+
# access token — the `id_token token` response type, which OpenID Connect
|
|
7
|
+
# Core §3.2 defines as part of the Implicit Flow, not the Hybrid Flow of §3.3
|
|
8
|
+
# (whose response types this gem does not implement).
|
|
9
|
+
#
|
|
10
|
+
# The host object must implement `#claims`, expose the access token via an
|
|
11
|
+
# `#access_token` reader, and resolve its signing key via `#selected_key` —
|
|
12
|
+
# all provided by `Doorkeeper::OpenidConnect::IdToken`, which configured
|
|
13
|
+
# `id_token_class` overrides must inherit from.
|
|
14
|
+
module AtHashConcern
|
|
10
15
|
def claims
|
|
11
16
|
super.merge(at_hash: at_hash)
|
|
12
17
|
end
|
|
@@ -37,8 +42,12 @@ module Doorkeeper
|
|
|
37
42
|
Base64.urlsafe_encode64(first_half).tr("=", "")
|
|
38
43
|
end
|
|
39
44
|
|
|
45
|
+
# OIDC Core §3.2.2.10 requires the digest to match the `alg` of the ID
|
|
46
|
+
# Token's actual JOSE header — derived from the key the token is signed
|
|
47
|
+
# with (`selected_key`, shared with `as_jws_token`), not from the global
|
|
48
|
+
# `signing_algorithm`, which a `select_key` override may diverge from.
|
|
40
49
|
def at_hash_digest
|
|
41
|
-
case
|
|
50
|
+
case selected_key.algorithm.to_s
|
|
42
51
|
when /256\z/ then Digest::SHA256
|
|
43
52
|
when /384\z/ then Digest::SHA384
|
|
44
53
|
when /512\z/ then Digest::SHA512
|
|
@@ -18,13 +18,20 @@ module Doorkeeper
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
Doorkeeper::OpenidConnect.configuration.claims.to_h.map do |name, claim|
|
|
21
|
-
if claim
|
|
22
|
-
claim.response.include?(response)
|
|
21
|
+
if claim_applies?(claim, access_token, response)
|
|
23
22
|
[name, claim.generator.call(resource_owner, access_token.scopes, access_token)]
|
|
24
23
|
end
|
|
25
24
|
end.compact.to_h
|
|
26
25
|
end
|
|
27
26
|
|
|
27
|
+
# A claim is included when the token carries one of its scopes and it is
|
|
28
|
+
# configured for this response type (:id_token / :user_info).
|
|
29
|
+
def self.claim_applies?(claim, access_token, response)
|
|
30
|
+
claim.scopes.any? { |scope| access_token.scopes.exists?(scope) } &&
|
|
31
|
+
claim.response.include?(response)
|
|
32
|
+
end
|
|
33
|
+
private_class_method :claim_applies?
|
|
34
|
+
|
|
28
35
|
def initialize(&block)
|
|
29
36
|
@claims = OpenStruct.new
|
|
30
37
|
instance_eval(&block)
|
|
@@ -68,15 +68,6 @@ module Doorkeeper
|
|
|
68
68
|
def build
|
|
69
69
|
@config
|
|
70
70
|
end
|
|
71
|
-
|
|
72
|
-
def jws_public_key(*_args)
|
|
73
|
-
warn "DEPRECATION WARNING: `jws_public_key` is not needed anymore and will be removed in a future version, please remove it from config/initializers/doorkeeper_openid_connect.rb"
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def jws_private_key(*args)
|
|
77
|
-
warn "DEPRECATION WARNING: `jws_private_key` has been replaced by `signing_key` and will be removed in a future version, please remove it from config/initializers/doorkeeper_openid_connect.rb"
|
|
78
|
-
signing_key(*args)
|
|
79
|
-
end
|
|
80
71
|
end
|
|
81
72
|
|
|
82
73
|
mattr_reader(:builder_class) { Config::Builder }
|
|
@@ -140,15 +131,19 @@ module Doorkeeper
|
|
|
140
131
|
option :open_id_request_class, default: "Doorkeeper::OpenidConnect::Request"
|
|
141
132
|
|
|
142
133
|
# A class that provides custom behavior for generating ID tokens.
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
134
|
+
# Must inherit from `Doorkeeper::OpenidConnect::IdToken`, which carries the
|
|
135
|
+
# security-critical invariants (required-claim enforcement, the merge order that keeps the
|
|
136
|
+
# `claims` configuration block from overriding `sub`/`aud`/`exp`, nonce and `at_hash`
|
|
137
|
+
# handling), so a subclass only overrides what it actually needs — typically `claims`
|
|
138
|
+
# (call `super.merge(...)` so the registered claims are always present; a key you add that
|
|
139
|
+
# collides with one of them does replace it, so keep your keys distinct unless replacing it
|
|
140
|
+
# is what you mean), `audience`, or `select_key` for custom signing-key selection.
|
|
147
141
|
option :id_token_class, default: "Doorkeeper::OpenidConnect::IdToken"
|
|
148
142
|
|
|
149
143
|
# A class that provides custom behavior for generating the UserInfo response.
|
|
150
|
-
#
|
|
151
|
-
#
|
|
144
|
+
# Must inherit from `Doorkeeper::OpenidConnect::UserInfo`; a subclass typically only
|
|
145
|
+
# overrides `claims` (call `super.merge(...)`, and leave `sub` out of the merged hash unless
|
|
146
|
+
# replacing the canonical subject identifier is what you mean).
|
|
152
147
|
option :user_info_class, default: "Doorkeeper::OpenidConnect::UserInfo"
|
|
153
148
|
|
|
154
149
|
# Doorkeeper OpenID Request model class.
|
|
@@ -160,26 +155,32 @@ module Doorkeeper
|
|
|
160
155
|
end
|
|
161
156
|
|
|
162
157
|
def id_token_model
|
|
163
|
-
resolve_validated_model(:id_token, id_token_class,
|
|
158
|
+
resolve_validated_model(:id_token, id_token_class, IdToken)
|
|
164
159
|
end
|
|
165
160
|
|
|
166
161
|
def user_info_model
|
|
167
|
-
resolve_validated_model(:user_info, user_info_class,
|
|
162
|
+
resolve_validated_model(:user_info, user_info_class, UserInfo)
|
|
168
163
|
end
|
|
169
164
|
|
|
170
165
|
private
|
|
171
166
|
|
|
172
167
|
# Resolves an `id_token_class` / `user_info_class` override to its class
|
|
173
|
-
# and validates that
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
|
|
168
|
+
# and validates that it inherits from the corresponding default. The
|
|
169
|
+
# ancestry check replaced the earlier method-presence list: presence
|
|
170
|
+
# could be satisfied by any class (ActiveSupport defines `as_json` on
|
|
171
|
+
# `Object`), while inheritance also carries the security-critical
|
|
172
|
+
# behavior — required-claim enforcement, the claim merge order, nonce
|
|
173
|
+
# and `at_hash` handling — that a from-scratch implementation would
|
|
174
|
+
# have to reproduce. Both resolution and validation happen lazily at
|
|
175
|
+
# first use rather than inside `Doorkeeper::OpenidConnect.configure`:
|
|
176
|
+
# constantizing an app-defined class while initializers run breaks
|
|
177
|
+
# zeitwerk on Rails 7+, because reloadable constants must not be
|
|
178
|
+
# referenced during boot — the same reason `open_id_request_model`
|
|
179
|
+
# constantizes lazily. The class is also deliberately not memoized, so
|
|
180
|
+
# code reloading in development never hands back a stale class; only
|
|
181
|
+
# the validation result is cached, keyed on the resolved class so a
|
|
182
|
+
# reloaded class is re-validated.
|
|
183
|
+
def resolve_validated_model(kind, class_name, base_model)
|
|
183
184
|
# `safe_constantize` (unlike a bare `constantize` rescue) only reports
|
|
184
185
|
# nil when the configured constant itself is missing; a NameError
|
|
185
186
|
# raised while loading the class body still surfaces as-is.
|
|
@@ -192,18 +193,15 @@ module Doorkeeper
|
|
|
192
193
|
@validated_models ||= {}
|
|
193
194
|
return model if @validated_models[kind] == model
|
|
194
195
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
validate_model_inheritance!(kind, class_name, model, base_model)
|
|
197
|
+
@validated_models[kind] = model
|
|
198
|
+
end
|
|
198
199
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
"The configured #{kind}_class (#{class_name}) is missing the following " \
|
|
202
|
-
"required methods: #{missing_methods.join(", ")}"
|
|
203
|
-
end
|
|
200
|
+
def validate_model_inheritance!(kind, class_name, model, base_model)
|
|
201
|
+
return if model.is_a?(Class) && model <= base_model
|
|
204
202
|
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
raise Errors::InvalidConfiguration,
|
|
204
|
+
"The configured #{kind}_class (#{class_name}) must inherit from #{base_model.name}"
|
|
207
205
|
end
|
|
208
206
|
end
|
|
209
207
|
end
|
|
@@ -17,23 +17,13 @@ module Doorkeeper
|
|
|
17
17
|
# itself gates the emission on redirectability and on the issuer
|
|
18
18
|
# being configured. Versions predating #1849 simply ignore the
|
|
19
19
|
# attribute, so no version guard is needed.
|
|
20
|
-
error_response =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
28
|
-
else
|
|
29
|
-
::Doorkeeper::OAuth::ErrorResponse.new(
|
|
30
|
-
name: exception.type,
|
|
31
|
-
state: params[:state],
|
|
32
|
-
redirect_uri: params[:redirect_uri],
|
|
33
|
-
response_on_fragment: pre_auth.response_on_fragment?,
|
|
34
|
-
issuer: Doorkeeper::OpenidConnect.doorkeeper_issuer,
|
|
35
|
-
)
|
|
36
|
-
end
|
|
20
|
+
error_response = oidc_error_response_class(exception).new(
|
|
21
|
+
name: exception.type,
|
|
22
|
+
state: params[:state],
|
|
23
|
+
redirect_uri: params[:redirect_uri],
|
|
24
|
+
response_on_fragment: pre_auth.response_on_fragment?,
|
|
25
|
+
issuer: Doorkeeper::OpenidConnect.doorkeeper_issuer,
|
|
26
|
+
)
|
|
37
27
|
|
|
38
28
|
response.headers.merge!(error_response.headers)
|
|
39
29
|
|
|
@@ -45,6 +35,14 @@ module Doorkeeper
|
|
|
45
35
|
@authorize_response = error_response
|
|
46
36
|
redirect_or_render(@authorize_response)
|
|
47
37
|
end
|
|
38
|
+
|
|
39
|
+
def oidc_error_response_class(exception)
|
|
40
|
+
if exception.type == :invalid_request
|
|
41
|
+
::Doorkeeper::OAuth::InvalidRequestResponse
|
|
42
|
+
else
|
|
43
|
+
::Doorkeeper::OAuth::ErrorResponse
|
|
44
|
+
end
|
|
45
|
+
end
|
|
48
46
|
end
|
|
49
47
|
end
|
|
50
48
|
end
|
|
@@ -22,18 +22,12 @@ module Doorkeeper
|
|
|
22
22
|
|
|
23
23
|
def apply_oidc_prompt!(prompt, prompt_values, owner)
|
|
24
24
|
case prompt
|
|
25
|
-
when "none"
|
|
26
|
-
|
|
27
|
-
when "
|
|
28
|
-
|
|
29
|
-
when "
|
|
30
|
-
|
|
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
|
|
25
|
+
when "none" then handle_oidc_prompt_none!(prompt_values, owner)
|
|
26
|
+
when "login" then handle_oidc_prompt_login!(owner)
|
|
27
|
+
when "consent" then handle_oidc_prompt_consent!(owner)
|
|
28
|
+
when "select_account" then select_account_for_oidc_resource_owner(owner)
|
|
29
|
+
when "create" then nil # NOTE: not supported, but does not raise an error.
|
|
30
|
+
else raise Errors::InvalidRequest
|
|
37
31
|
end
|
|
38
32
|
end
|
|
39
33
|
|
|
@@ -81,6 +75,11 @@ module Doorkeeper
|
|
|
81
75
|
|
|
82
76
|
def return_without_oidc_prompt_param(prompt_value)
|
|
83
77
|
return_to = URI.parse(request.path)
|
|
78
|
+
return_to.query = query_parameters_without_oidc_prompt(prompt_value).to_query
|
|
79
|
+
return_to.to_s
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def query_parameters_without_oidc_prompt(prompt_value)
|
|
84
83
|
# Work on a copy: `request.query_parameters` is memoized and shared, so
|
|
85
84
|
# mutating it in place would corrupt the parameters seen by the rest of
|
|
86
85
|
# the request (and any subsequent prompt value in the same loop).
|
|
@@ -95,8 +94,7 @@ module Doorkeeper
|
|
|
95
94
|
else
|
|
96
95
|
query.delete("prompt")
|
|
97
96
|
end
|
|
98
|
-
|
|
99
|
-
return_to.to_s
|
|
97
|
+
query
|
|
100
98
|
end
|
|
101
99
|
|
|
102
100
|
def reauthenticate_oidc_resource_owner(owner)
|
|
@@ -35,14 +35,10 @@ module Doorkeeper
|
|
|
35
35
|
relation = token_model.authorized_tokens_for(pre_auth.client.id, owner)
|
|
36
36
|
batch_size = Doorkeeper.configuration.token_lookup_batch_size
|
|
37
37
|
|
|
38
|
-
match_found = false
|
|
39
38
|
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
|
|
39
|
+
return true if batch.any? { |token| token.scopes.scopes?(pre_auth.scopes) }
|
|
44
40
|
end
|
|
45
|
-
|
|
41
|
+
false
|
|
46
42
|
end
|
|
47
43
|
|
|
48
44
|
# Force Doorkeeper's `render_success` onto the auto-issue path when a
|
|
@@ -9,10 +9,16 @@ module Doorkeeper
|
|
|
9
9
|
# must never be silently dropped when blank.
|
|
10
10
|
REQUIRED_CLAIMS = %i[iss sub aud exp iat].freeze
|
|
11
11
|
|
|
12
|
+
# The return type of `select_key`: the key material and the algorithm it
|
|
13
|
+
# signs with, kept together so `at_hash` can always be computed with the
|
|
14
|
+
# digest matching the algorithm in the token's actual JOSE header
|
|
15
|
+
# (OIDC Core §3.2.2.10).
|
|
16
|
+
SigningKey = Struct.new(:keypair, :kid, :algorithm, keyword_init: true)
|
|
17
|
+
|
|
12
18
|
# `resource_owner` is exposed so callers can detect a token whose owner no
|
|
13
19
|
# longer resolves (e.g. deleted after issuance) before serializing — the
|
|
14
20
|
# `sub` claim would otherwise dereference a nil owner and raise.
|
|
15
|
-
# `access_token` is the reader
|
|
21
|
+
# `access_token` is the reader AtHashConcern relies on to compute
|
|
16
22
|
# `at_hash`; a custom id_token_class must expose one as well.
|
|
17
23
|
attr_reader :access_token, :nonce, :resource_owner
|
|
18
24
|
|
|
@@ -58,10 +64,31 @@ module Doorkeeper
|
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
def as_jws_token
|
|
67
|
+
key = selected_key
|
|
68
|
+
|
|
61
69
|
::JWT.encode(as_json,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
{ typ: "JWT", kid:
|
|
70
|
+
key.keypair,
|
|
71
|
+
key.algorithm.to_s,
|
|
72
|
+
{ typ: "JWT", kid: key.kid }).to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Override point for custom signing-key selection (per-client keys, key
|
|
76
|
+
# rotation, multi-tenant setups, …). Must return an object responding to
|
|
77
|
+
# `#keypair`, `#kid` and `#algorithm` — use `SigningKey` for convenience.
|
|
78
|
+
# Keys returned from here are not advertised automatically: a custom
|
|
79
|
+
# implementation is responsible for exposing any additional keys through
|
|
80
|
+
# its own JWKS handling so clients can validate the signature.
|
|
81
|
+
def select_key
|
|
82
|
+
# Resolved once: `signing_key` builds a fresh JWK per call and honors
|
|
83
|
+
# callable configuration, so reading `keypair` and `kid` from separate
|
|
84
|
+
# calls could pair values from two different keys.
|
|
85
|
+
jwk = Doorkeeper::OpenidConnect.signing_key
|
|
86
|
+
|
|
87
|
+
SigningKey.new(
|
|
88
|
+
keypair: jwk.keypair,
|
|
89
|
+
kid: jwk.kid,
|
|
90
|
+
algorithm: Doorkeeper::OpenidConnect.signing_algorithm.to_s,
|
|
91
|
+
)
|
|
65
92
|
end
|
|
66
93
|
|
|
67
94
|
# Public: the RFC 9207 `iss` authorization response parameter must be
|
|
@@ -78,6 +105,15 @@ module Doorkeeper
|
|
|
78
105
|
|
|
79
106
|
private
|
|
80
107
|
|
|
108
|
+
# `select_key` resolved exactly once per token, mirroring the `issuer`
|
|
109
|
+
# memoization above: the signature (`as_jws_token`) and the `at_hash`
|
|
110
|
+
# digest (`AtHashConcern`) must agree on the algorithm, so a
|
|
111
|
+
# dynamic `select_key` implementation must not be re-invoked between
|
|
112
|
+
# the two.
|
|
113
|
+
def selected_key
|
|
114
|
+
@selected_key ||= select_key
|
|
115
|
+
end
|
|
116
|
+
|
|
81
117
|
def subject
|
|
82
118
|
Doorkeeper::OpenidConnect.configuration.subject.call(
|
|
83
119
|
@resource_owner,
|
|
@@ -65,9 +65,9 @@ module Doorkeeper
|
|
|
65
65
|
server_scopes = server.scopes
|
|
66
66
|
|
|
67
67
|
if server_scopes.respond_to?(:allowed)
|
|
68
|
-
server_scopes.allowed(
|
|
68
|
+
server_scopes.allowed(effective_requested_scopes).to_s
|
|
69
69
|
else
|
|
70
|
-
(server_scopes &
|
|
70
|
+
(server_scopes & effective_requested_scopes).to_s
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
|
|
@@ -121,12 +121,34 @@ module Doorkeeper
|
|
|
121
121
|
::Doorkeeper::OAuth::Scopes.from_string(@params[:scope].to_s)
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
+
# RFC 7591 §2: when the request omits `scope`, the authorization
|
|
125
|
+
# server registers the client with its own default set. Doorkeeper
|
|
126
|
+
# has one — `default_scopes` — and registering with no scopes at all
|
|
127
|
+
# is emphatically not it: an application whose `scopes` column is
|
|
128
|
+
# blank is treated as unrestricted by Doorkeeper's ScopeChecker
|
|
129
|
+
# (`app_scopes.presence || server_scopes`), so an omitted `scope`
|
|
130
|
+
# would register a client entitled to every scope the server defines,
|
|
131
|
+
# while a client that honestly declared a narrow scope stays confined
|
|
132
|
+
# to it.
|
|
133
|
+
def effective_requested_scopes
|
|
134
|
+
requested_scopes.presence || server.default_scopes
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# A registration must never persist an empty scope set, whichever way
|
|
138
|
+
# it got there: neither an explicit `scope` made up entirely of
|
|
139
|
+
# unsupported values, nor an omitted `scope` on a server that
|
|
140
|
+
# configures no default scopes.
|
|
124
141
|
def validate_scope
|
|
125
|
-
return true if
|
|
142
|
+
return true if permitted_scopes.present?
|
|
126
143
|
|
|
127
144
|
@error_description =
|
|
128
|
-
|
|
129
|
-
|
|
145
|
+
if requested_scopes.present?
|
|
146
|
+
"scope '#{requested_scopes}' contains no scopes supported by this server. " \
|
|
147
|
+
"Supported scopes: #{server.scopes}"
|
|
148
|
+
else
|
|
149
|
+
"scope is required: this server registers no default scopes, so a client " \
|
|
150
|
+
"must request at least one of its supported scopes: #{server.scopes}"
|
|
151
|
+
end
|
|
130
152
|
false
|
|
131
153
|
end
|
|
132
154
|
|
|
@@ -26,33 +26,38 @@ module Doorkeeper
|
|
|
26
26
|
|
|
27
27
|
def generate_routes!(options)
|
|
28
28
|
@mapping = Mapper.new.map(&@block)
|
|
29
|
-
openid_connect = ::Doorkeeper::OpenidConnect.configuration
|
|
30
|
-
prefix = route_helper_prefix(options)
|
|
31
29
|
|
|
32
30
|
routes.scope options[:scope] || "oauth", as: "oauth" do
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
map_oauth_routes
|
|
32
|
+
end
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
end
|
|
34
|
+
routes.scope(**well_known_scope(options)) do
|
|
35
|
+
map_route(:discovery, :discovery_well_known_routes)
|
|
39
36
|
end
|
|
37
|
+
end
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def map_oauth_routes
|
|
42
|
+
map_route(:userinfo, :userinfo_routes)
|
|
43
|
+
map_route(:discovery, :discovery_routes)
|
|
44
|
+
return unless ::Doorkeeper::OpenidConnect.configuration.dynamic_client_registration
|
|
45
|
+
|
|
46
|
+
map_route(:dynamic_client_registration, :dynamic_client_registration_routes)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def well_known_scope(options)
|
|
50
|
+
prefix = route_helper_prefix(options)
|
|
51
|
+
scope = { as: "oauth" }
|
|
42
52
|
# When the engine is mounted under a named scope (e.g.
|
|
43
53
|
# `scope :users, as: :users`), Doorkeeper's and this engine's URL
|
|
44
54
|
# helpers are generated with that prefix (`users_oauth_*`). Pass the
|
|
45
55
|
# prefix down to the discovery controller via a route default so it can
|
|
46
56
|
# resolve the correct namespaced helpers for the published endpoints.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
routes.scope(**well_known_scope) do
|
|
50
|
-
map_route(:discovery, :discovery_well_known_routes)
|
|
51
|
-
end
|
|
57
|
+
scope[:defaults] = { route_helper_prefix: prefix } if prefix.present?
|
|
58
|
+
scope
|
|
52
59
|
end
|
|
53
60
|
|
|
54
|
-
private
|
|
55
|
-
|
|
56
61
|
def route_helper_prefix(options)
|
|
57
62
|
name = options[:as]
|
|
58
63
|
name.present? ? "#{name}_" : ""
|
|
@@ -17,11 +17,11 @@ require "jwt"
|
|
|
17
17
|
# `ClaimsBuilder` while its class body runs.
|
|
18
18
|
module Doorkeeper
|
|
19
19
|
module OpenidConnect
|
|
20
|
+
autoload :AtHashConcern, "doorkeeper/openid_connect/at_hash_concern"
|
|
20
21
|
autoload :ClaimsBuilder, "doorkeeper/openid_connect/claims_builder"
|
|
21
22
|
autoload :DiscoveryHelpersMixin, "doorkeeper/openid_connect/discovery_helpers_mixin"
|
|
22
23
|
autoload :Errors, "doorkeeper/openid_connect/errors"
|
|
23
24
|
autoload :GrantTypesSupportedMixin, "doorkeeper/openid_connect/grant_types_supported_mixin"
|
|
24
|
-
autoload :HybridIdTokenConcern, "doorkeeper/openid_connect/hybrid_id_token_concern"
|
|
25
25
|
autoload :IdToken, "doorkeeper/openid_connect/id_token"
|
|
26
26
|
autoload :TokenEndpointAuthMethodsSupportedMixin,
|
|
27
27
|
"doorkeeper/openid_connect/token_endpoint_auth_methods_supported_mixin"
|
|
@@ -45,8 +45,8 @@ module Doorkeeper
|
|
|
45
45
|
# makes them safe to autoload from a namespace this gem does not own.
|
|
46
46
|
#
|
|
47
47
|
# `Request::IdToken` and `Request::IdTokenToken` reach these at request time;
|
|
48
|
-
# for an application that never enables the OpenID Connect implicit
|
|
49
|
-
#
|
|
48
|
+
# for an application that never enables the OpenID Connect implicit flows,
|
|
49
|
+
# none of the four is ever loaded.
|
|
50
50
|
module OAuth
|
|
51
51
|
autoload :IdTokenRequest, "doorkeeper/oauth/id_token_request"
|
|
52
52
|
autoload :IdTokenResponse, "doorkeeper/oauth/id_token_response"
|
|
@@ -298,14 +298,10 @@ module Doorkeeper
|
|
|
298
298
|
return issuer unless issuer.respond_to?(:call)
|
|
299
299
|
|
|
300
300
|
case issuer.arity
|
|
301
|
-
when 0
|
|
302
|
-
|
|
303
|
-
when
|
|
304
|
-
|
|
305
|
-
when 2
|
|
306
|
-
issuer.call(resource_owner, application)
|
|
307
|
-
else
|
|
308
|
-
issuer.call(resource_owner, application, request)
|
|
301
|
+
when 0 then issuer.call
|
|
302
|
+
when 1 then issuer.call(request || resource_owner)
|
|
303
|
+
when 2 then issuer.call(resource_owner, application)
|
|
304
|
+
else issuer.call(resource_owner, application, request)
|
|
309
305
|
end
|
|
310
306
|
end
|
|
311
307
|
private_class_method :call_issuer
|
|
@@ -208,12 +208,20 @@ Doorkeeper::OpenidConnect.configure do
|
|
|
208
208
|
# different apps may need different claim sets) or if you need to transform data before returning
|
|
209
209
|
# it to external clients.
|
|
210
210
|
#
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
#
|
|
216
|
-
#
|
|
211
|
+
# Custom classes must inherit from Doorkeeper::OpenidConnect::IdToken /
|
|
212
|
+
# Doorkeeper::OpenidConnect::UserInfo (validated when the class is first used). The base classes
|
|
213
|
+
# carry the security-critical behavior — required-claim enforcement, the claim merge order that
|
|
214
|
+
# keeps the `claims` configuration block from overriding `sub`/`aud`/`exp`, nonce and `at_hash`
|
|
215
|
+
# handling — so a subclass only overrides what it actually needs. Typical override points:
|
|
216
|
+
#
|
|
217
|
+
# - `claims` — call `super.merge(...)` so the base claims are always present. A key you add
|
|
218
|
+
# that collides with a registered claim replaces it, so keep your keys distinct unless
|
|
219
|
+
# replacing it is what you mean
|
|
220
|
+
# - `audience` (ID Token) — the `aud` claim may be a string or an array per OIDC Core
|
|
221
|
+
# - `select_key` (ID Token) — return a Doorkeeper::OpenidConnect::IdToken::SigningKey to sign
|
|
222
|
+
# with a different key/algorithm (per-client keys, rotation, multi-tenancy). The `at_hash`
|
|
223
|
+
# digest follows the selected algorithm automatically; advertising any additional keys via
|
|
224
|
+
# JWKS is the application's responsibility.
|
|
217
225
|
#
|
|
218
226
|
# Refer to Doorkeeper::OpenidConnect::IdToken and Doorkeeper::OpenidConnect::UserInfo for more
|
|
219
227
|
# information and implementation details.
|
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: 2.0.0
|
|
4
|
+
version: 2.0.0
|
|
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-
|
|
13
|
+
date: 2026-09-22 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: doorkeeper
|
|
@@ -195,6 +195,7 @@ files:
|
|
|
195
195
|
- lib/doorkeeper/oauth/id_token_token_request.rb
|
|
196
196
|
- lib/doorkeeper/oauth/id_token_token_response.rb
|
|
197
197
|
- lib/doorkeeper/openid_connect.rb
|
|
198
|
+
- lib/doorkeeper/openid_connect/at_hash_concern.rb
|
|
198
199
|
- lib/doorkeeper/openid_connect/claims/claim.rb
|
|
199
200
|
- lib/doorkeeper/openid_connect/claims/normal_claim.rb
|
|
200
201
|
- lib/doorkeeper/openid_connect/claims_builder.rb
|
|
@@ -208,7 +209,6 @@ files:
|
|
|
208
209
|
- lib/doorkeeper/openid_connect/helpers/controller/max_age.rb
|
|
209
210
|
- lib/doorkeeper/openid_connect/helpers/controller/prompt.rb
|
|
210
211
|
- lib/doorkeeper/openid_connect/helpers/controller/token_matching.rb
|
|
211
|
-
- lib/doorkeeper/openid_connect/hybrid_id_token_concern.rb
|
|
212
212
|
- lib/doorkeeper/openid_connect/id_token.rb
|
|
213
213
|
- lib/doorkeeper/openid_connect/oauth/authorization/code.rb
|
|
214
214
|
- lib/doorkeeper/openid_connect/oauth/authorization_code_request.rb
|
|
@@ -256,9 +256,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
256
256
|
version: '3.2'
|
|
257
257
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
258
258
|
requirements:
|
|
259
|
-
- - "
|
|
259
|
+
- - ">="
|
|
260
260
|
- !ruby/object:Gem::Version
|
|
261
|
-
version:
|
|
261
|
+
version: '0'
|
|
262
262
|
requirements: []
|
|
263
263
|
rubygems_version: 3.1.6
|
|
264
264
|
signing_key:
|