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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +1 -1
- data/app/controllers/concerns/doorkeeper/openid_connect/authorizations_extension.rb +5 -0
- data/app/controllers/doorkeeper/openid_connect/discovery_controller.rb +4 -0
- data/lib/doorkeeper/openid_connect/helpers/controller/error_response.rb +45 -0
- data/lib/doorkeeper/openid_connect/helpers/controller/max_age.rb +69 -0
- data/lib/doorkeeper/openid_connect/helpers/controller/prompt.rb +113 -0
- data/lib/doorkeeper/openid_connect/helpers/controller/token_matching.rb +59 -0
- data/lib/doorkeeper/openid_connect/helpers/controller.rb +18 -207
- data/lib/doorkeeper/openid_connect/oauth/authorization_code_request.rb +5 -0
- data/lib/doorkeeper/openid_connect/oauth/token_response.rb +4 -0
- data/lib/doorkeeper/openid_connect/orm/active_record.rb +7 -0
- data/lib/doorkeeper/openid_connect/version.rb +1 -1
- data/lib/generators/doorkeeper/openid_connect/templates/initializer.rb +57 -0
- metadata +6 -4
- data/lib/doorkeeper/openid_connect/claims/aggregated_claim.rb +0 -11
- data/lib/doorkeeper/openid_connect/claims/distributed_claim.rb +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc9243cc386621bb9b9474617c885f398632d60fdeabc7ecb50deab1364bb787
|
|
4
|
+
data.tar.gz: e33f32f4992a35ec43762c4c5c60d0ba6e98bb8b4f7c60c8cd0d4d69b4c1f535
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
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
|
)
|
|
@@ -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
|
+
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-
|
|
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
|