doorkeeper 5.9.3 → 6.0.0.beta1
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 +41 -0
- data/README.md +1 -0
- data/app/controllers/doorkeeper/metadata_controller.rb +20 -0
- data/app/controllers/doorkeeper/tokens_controller.rb +10 -1
- data/app/views/doorkeeper/authorizations/new.html.erb +6 -0
- data/config/locales/en.yml +1 -0
- data/lib/doorkeeper/client_authentication/credentials.rb +11 -0
- data/lib/doorkeeper/client_authentication/fallback_method.rb +19 -0
- data/lib/doorkeeper/client_authentication/legacy_callable.rb +46 -0
- data/lib/doorkeeper/client_authentication/method.rb +23 -0
- data/lib/doorkeeper/client_authentication/registry.rb +47 -0
- data/lib/doorkeeper/client_authentication.rb +93 -0
- data/lib/doorkeeper/config/option.rb +1 -1
- data/lib/doorkeeper/config/validations.rb +162 -1
- data/lib/doorkeeper/config.rb +114 -9
- data/lib/doorkeeper/errors.rb +14 -0
- data/lib/doorkeeper/helpers/controller.rb +2 -1
- data/lib/doorkeeper/models/access_token_mixin.rb +48 -8
- data/lib/doorkeeper/models/application_mixin.rb +14 -6
- data/lib/doorkeeper/models/concerns/secret_storable.rb +10 -1
- data/lib/doorkeeper/oauth/authorization/uri_builder.rb +11 -0
- data/lib/doorkeeper/oauth/authorization_code_request.rb +4 -1
- data/lib/doorkeeper/oauth/client.rb +18 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +53 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_post.rb +29 -0
- data/lib/doorkeeper/oauth/client_authentication/none.rb +32 -0
- data/lib/doorkeeper/oauth/client_credentials/creator.rb +4 -1
- data/lib/doorkeeper/oauth/code_response.rb +16 -2
- data/lib/doorkeeper/oauth/error_response.rb +11 -2
- data/lib/doorkeeper/oauth/metadata_response.rb +157 -0
- data/lib/doorkeeper/oauth/pre_authorization.rb +12 -5
- data/lib/doorkeeper/oauth/token_introspection.rb +27 -7
- data/lib/doorkeeper/orm/active_record/mixins/application.rb +2 -2
- data/lib/doorkeeper/rails/routes/mapping.rb +1 -0
- data/lib/doorkeeper/rails/routes.rb +6 -0
- data/lib/doorkeeper/request.rb +59 -0
- data/lib/doorkeeper/server.rb +5 -2
- data/lib/doorkeeper/version.rb +4 -4
- data/lib/doorkeeper.rb +8 -4
- data/lib/generators/doorkeeper/templates/initializer.rb +66 -6
- metadata +12 -2
- data/lib/doorkeeper/oauth/client/credentials.rb +0 -34
data/lib/doorkeeper/config.rb
CHANGED
|
@@ -65,13 +65,50 @@ module Doorkeeper
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
# Change the way client credentials are retrieved from the request object.
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
68
|
+
#
|
|
69
|
+
# @deprecated Use the +client_authentication+ option instead. The legacy
|
|
70
|
+
# +:from_basic+ / +:from_params+ methods are automatically converted to
|
|
71
|
+
# the +:client_secret_basic+ / +:client_secret_post+ authentication
|
|
72
|
+
# methods. +:none+ (public client support) is appended only when
|
|
73
|
+
# +:from_params+ was configured, since that is the only legacy method
|
|
74
|
+
# that accepted a bare +client_id+ without a secret — +:from_basic+ on
|
|
75
|
+
# its own never did, so it is not broadened. Callable extractors are
|
|
76
|
+
# wrapped in a legacy adapter so they keep working during the
|
|
77
|
+
# deprecation window.
|
|
71
78
|
#
|
|
72
79
|
# @param methods [Array] Define client credentials
|
|
73
80
|
def client_credentials(*methods)
|
|
74
|
-
|
|
81
|
+
deprecated(
|
|
82
|
+
"client_credentials",
|
|
83
|
+
"Use the client_authentication option instead. Automatically converting to client_authentication",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
client_authentication = Doorkeeper::ClientAuthentication.from_legacy_client_credentials(methods)
|
|
87
|
+
|
|
88
|
+
if client_authentication.empty?
|
|
89
|
+
Kernel.warn(
|
|
90
|
+
"[DOORKEEPER] No known client_credentials method detected, " \
|
|
91
|
+
"cannot automatically convert to client_authentication option",
|
|
92
|
+
)
|
|
93
|
+
else
|
|
94
|
+
@config.instance_variable_set(:@client_credentials_methods, client_authentication)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Declare which client authentication methods (RFC 6749 §2.3) are
|
|
99
|
+
# accepted and the order in which they are tried. Accepts either an array
|
|
100
|
+
# or varargs, so both forms are honoured exactly as written:
|
|
101
|
+
#
|
|
102
|
+
# client_authentication %i[client_secret_basic client_secret_post none]
|
|
103
|
+
# client_authentication :client_secret_basic, :client_secret_post
|
|
104
|
+
#
|
|
105
|
+
# Unlike the deprecated +client_credentials+ option, the listed methods
|
|
106
|
+
# are used verbatim — nothing (in particular +:none+) is appended, so a
|
|
107
|
+
# restrictive configuration is never silently broadened.
|
|
108
|
+
#
|
|
109
|
+
# @param methods [Array<Symbol>] the client authentication method names
|
|
110
|
+
def client_authentication(*methods)
|
|
111
|
+
@config.instance_variable_set(:@client_authentication, methods.flatten)
|
|
75
112
|
end
|
|
76
113
|
|
|
77
114
|
# Change the way access token is authenticated from the request object.
|
|
@@ -202,6 +239,13 @@ module Doorkeeper
|
|
|
202
239
|
|
|
203
240
|
private
|
|
204
241
|
|
|
242
|
+
def deprecated(name, message = nil)
|
|
243
|
+
warning = "[DOORKEEPER] #{name} has been deprecated and will soon be removed"
|
|
244
|
+
warning = "#{warning}\n#{message}" if message.present?
|
|
245
|
+
|
|
246
|
+
Kernel.warn(warning)
|
|
247
|
+
end
|
|
248
|
+
|
|
205
249
|
# Configure the secret storing functionality
|
|
206
250
|
def configure_secrets_for(type, using:, fallback:)
|
|
207
251
|
raise ArgumentError, "Invalid type #{type}" if %i[application token].exclude?(type)
|
|
@@ -337,6 +381,10 @@ module Doorkeeper
|
|
|
337
381
|
#
|
|
338
382
|
option :realm, default: "Doorkeeper"
|
|
339
383
|
|
|
384
|
+
# Issuer URL advertised in the OAuth 2.0 Authorization Server Metadata
|
|
385
|
+
# (RFC 8414). When nil, the request base URL is used instead.
|
|
386
|
+
option :issuer, default: nil
|
|
387
|
+
|
|
340
388
|
# Forces the usage of the HTTPS protocol in non-native redirect uris
|
|
341
389
|
# (enabled by default in non-development environments). OAuth2
|
|
342
390
|
# delegates security in communication to the HTTPS protocol so it is
|
|
@@ -413,6 +461,10 @@ module Doorkeeper
|
|
|
413
461
|
option :application_class,
|
|
414
462
|
default: "Doorkeeper::Application"
|
|
415
463
|
|
|
464
|
+
# Allows setting a hash of custom data merged into the OAuth 2.0
|
|
465
|
+
# Authorization Server Metadata response (RFC 8414).
|
|
466
|
+
option :custom_metadata, default: {}
|
|
467
|
+
|
|
416
468
|
# Allows to set blank redirect URIs for Applications in case
|
|
417
469
|
# server configured to use URI-less grant flows.
|
|
418
470
|
#
|
|
@@ -596,8 +648,57 @@ module Doorkeeper
|
|
|
596
648
|
pkce_code_challenge_methods
|
|
597
649
|
end
|
|
598
650
|
|
|
651
|
+
# Resolves the configured client authentication methods (RFC 6749 §2.3)
|
|
652
|
+
# into the registered +Doorkeeper::ClientAuthentication::Method+ objects.
|
|
653
|
+
#
|
|
654
|
+
# Honors the deprecated +client_credentials+ option for backwards
|
|
655
|
+
# compatibility: if it was used it provides the source of truth, unless
|
|
656
|
+
# +client_authentication+ was also set explicitly, in which case the
|
|
657
|
+
# latter wins.
|
|
658
|
+
def client_authentication_methods
|
|
659
|
+
return @client_authentication_methods if defined?(@client_authentication_methods)
|
|
660
|
+
|
|
661
|
+
# When both the deprecated +client_credentials+ and the new
|
|
662
|
+
# +client_authentication+ are set, +client_authentication+ wins. The
|
|
663
|
+
# conflict is warned about at validation time (see Validations), not here,
|
|
664
|
+
# so the message is not swallowed by this memoised resolver.
|
|
665
|
+
only_legacy = instance_variable_defined?(:@client_credentials_methods) &&
|
|
666
|
+
!instance_variable_defined?(:@client_authentication)
|
|
667
|
+
names = only_legacy ? @client_credentials_methods : client_authentication
|
|
668
|
+
|
|
669
|
+
# Names configured more than once resolve to the same registered Method
|
|
670
|
+
# instance, so identity-based #uniq drops the duplicates (which would
|
|
671
|
+
# otherwise be matched against requests twice and advertised twice in
|
|
672
|
+
# the server metadata) while distinct legacy callable adapters survive.
|
|
673
|
+
@client_authentication_methods = names.filter_map do |name|
|
|
674
|
+
# Legacy callables are already wrapped as Method adapters (see #client_credentials).
|
|
675
|
+
name.is_a?(Doorkeeper::ClientAuthentication::Method) ? name : Doorkeeper::ClientAuthentication.get(name)
|
|
676
|
+
end.uniq
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# The configured client authentication method names (RFC 6749 §2.3),
|
|
680
|
+
# defaulting to the registry's DEFAULT_METHODS when not set.
|
|
681
|
+
def client_authentication
|
|
682
|
+
return Doorkeeper::ClientAuthentication::DEFAULT_METHODS.dup unless instance_variable_defined?(:@client_authentication)
|
|
683
|
+
|
|
684
|
+
@client_authentication
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# @deprecated Renamed to +client_authentication_methods+. This alias keeps
|
|
688
|
+
# external callers (e.g. doorkeeper-openid_connect) working for one release
|
|
689
|
+
# and will be removed afterwards. It returns the legacy symbol names
|
|
690
|
+
# (e.g. +:from_basic+) rather than the internal Method objects so that
|
|
691
|
+
# consumers mapping from those symbols keep working unchanged.
|
|
599
692
|
def client_credentials_methods
|
|
600
|
-
@
|
|
693
|
+
unless defined?(@client_credentials_methods_rename_warned)
|
|
694
|
+
Kernel.warn(
|
|
695
|
+
"[DOORKEEPER] Doorkeeper.config.client_credentials_methods has been renamed to " \
|
|
696
|
+
"client_authentication_methods and will be removed in a future version.",
|
|
697
|
+
)
|
|
698
|
+
@client_credentials_methods_rename_warned = true
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
Doorkeeper::ClientAuthentication.to_legacy_client_credentials_names(client_authentication_methods)
|
|
601
702
|
end
|
|
602
703
|
|
|
603
704
|
def access_token_methods
|
|
@@ -665,13 +766,17 @@ module Doorkeeper
|
|
|
665
766
|
def calculate_token_grant_types
|
|
666
767
|
types = grant_flows - ["implicit"]
|
|
667
768
|
types << "refresh_token" if refresh_token_enabled?
|
|
668
|
-
types
|
|
769
|
+
types.uniq
|
|
669
770
|
end
|
|
670
771
|
|
|
671
772
|
# Calculates grant flows configured by the user in Doorkeeper
|
|
672
773
|
# configuration considering registered aliases that is exposed
|
|
673
774
|
# to single or multiple other flows.
|
|
674
775
|
#
|
|
776
|
+
# The refresh_token flow is added implicitly when +use_refresh_token+
|
|
777
|
+
# is configured, so the result lists every enabled flow (useful for
|
|
778
|
+
# RFC 8414 authorization server metadata).
|
|
779
|
+
#
|
|
675
780
|
def calculate_grant_flows
|
|
676
781
|
configured_flows = grant_flows.map(&:to_s)
|
|
677
782
|
aliases = Doorkeeper::GrantFlow.aliases.keys.map(&:to_s)
|
|
@@ -683,6 +788,8 @@ module Doorkeeper
|
|
|
683
788
|
flows.concat(Doorkeeper::GrantFlow.expand_alias(flow_alias))
|
|
684
789
|
end
|
|
685
790
|
|
|
791
|
+
flows << "refresh_token" if refresh_token_enabled?
|
|
792
|
+
|
|
686
793
|
flows.flatten.uniq
|
|
687
794
|
end
|
|
688
795
|
|
|
@@ -713,9 +820,7 @@ module Doorkeeper
|
|
|
713
820
|
end
|
|
714
821
|
|
|
715
822
|
def calculate_token_grant_flows
|
|
716
|
-
|
|
717
|
-
flows << Doorkeeper::GrantFlow.get("refresh_token") if refresh_token_enabled?
|
|
718
|
-
flows
|
|
823
|
+
enabled_grant_flows.select(&:handles_grant_type?)
|
|
719
824
|
end
|
|
720
825
|
end
|
|
721
826
|
end
|
data/lib/doorkeeper/errors.rb
CHANGED
|
@@ -24,6 +24,19 @@ module Doorkeeper
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Raised when a request uses more than one client authentication method,
|
|
28
|
+
# which RFC 6749 §2.3 explicitly forbids ("The client MUST NOT use more
|
|
29
|
+
# than one authentication method in each request").
|
|
30
|
+
class MultipleClientAuthMethods < DoorkeeperError
|
|
31
|
+
def type
|
|
32
|
+
:invalid_request
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reason
|
|
36
|
+
:multiple_client_auth_methods
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
27
40
|
class MissingRequiredParameter < DoorkeeperError
|
|
28
41
|
attr_reader :missing_param
|
|
29
42
|
|
|
@@ -62,6 +75,7 @@ module Doorkeeper
|
|
|
62
75
|
UnableToGenerateToken = Class.new(DoorkeeperError)
|
|
63
76
|
TokenGeneratorNotFound = Class.new(DoorkeeperError)
|
|
64
77
|
NoOrmCleaner = Class.new(DoorkeeperError)
|
|
78
|
+
MissingConfigurationBuilderClass = Class.new(DoorkeeperError)
|
|
65
79
|
|
|
66
80
|
InvalidRequest = Class.new(BaseResponseError)
|
|
67
81
|
InvalidToken = Class.new(BaseResponseError)
|
|
@@ -56,7 +56,8 @@ module Doorkeeper
|
|
|
56
56
|
OAuth::InvalidRequestResponse.new(
|
|
57
57
|
name: exception.type,
|
|
58
58
|
state: params[:state],
|
|
59
|
-
missing_param: exception.missing_param,
|
|
59
|
+
missing_param: exception.try(:missing_param),
|
|
60
|
+
reason: exception.try(:reason),
|
|
60
61
|
)
|
|
61
62
|
else
|
|
62
63
|
OAuth::ErrorResponse.new(name: exception.type, state: params[:state])
|
|
@@ -89,15 +89,19 @@ module Doorkeeper
|
|
|
89
89
|
# @param custom_attributes [Nilable Hash]
|
|
90
90
|
# A nil value, or hash with keys corresponding to the custom attributes
|
|
91
91
|
# configured with the `custom_access_token_attributes` config option.
|
|
92
|
-
# A nil value will ignore custom attributes
|
|
92
|
+
# A nil value will ignore custom attributes, while an empty hash will
|
|
93
|
+
# only match tokens that have no custom attributes set.
|
|
94
|
+
#
|
|
95
|
+
# @yield [token] optional additional predicate a token must satisfy to
|
|
96
|
+
# count as a match (e.g. the refresh token requirement of the request).
|
|
93
97
|
#
|
|
94
98
|
# @return [Doorkeeper::AccessToken, nil] Access Token instance or
|
|
95
99
|
# nil if matching record was not found
|
|
96
100
|
#
|
|
97
|
-
def matching_token_for(application, resource_owner, scopes, custom_attributes: nil, include_expired: true)
|
|
101
|
+
def matching_token_for(application, resource_owner, scopes, custom_attributes: nil, include_expired: true, &filter)
|
|
98
102
|
tokens = authorized_tokens_for(application&.id, resource_owner)
|
|
99
103
|
tokens = tokens.not_expired unless include_expired
|
|
100
|
-
find_matching_token(tokens, application, custom_attributes, scopes)
|
|
104
|
+
find_matching_token(tokens, application, custom_attributes, scopes, &filter)
|
|
101
105
|
end
|
|
102
106
|
|
|
103
107
|
# Interface to enumerate access token records in batches in order not
|
|
@@ -124,12 +128,13 @@ module Doorkeeper
|
|
|
124
128
|
# @param custom_attributes [Nilable Hash]
|
|
125
129
|
# A nil value, or hash with keys corresponding to the custom attributes
|
|
126
130
|
# configured with the `custom_access_token_attributes` config option.
|
|
127
|
-
# A nil value will ignore custom attributes
|
|
131
|
+
# A nil value will ignore custom attributes, while an empty hash will
|
|
132
|
+
# only match tokens that have no custom attributes set.
|
|
128
133
|
#
|
|
129
134
|
# @return [Doorkeeper::AccessToken, nil] Access Token instance or
|
|
130
135
|
# nil if matching record was not found
|
|
131
136
|
#
|
|
132
|
-
def find_matching_token(relation, application, custom_attributes, scopes)
|
|
137
|
+
def find_matching_token(relation, application, custom_attributes, scopes, &filter)
|
|
133
138
|
return nil unless relation
|
|
134
139
|
|
|
135
140
|
matching_tokens = []
|
|
@@ -138,7 +143,8 @@ module Doorkeeper
|
|
|
138
143
|
find_access_token_in_batches(relation, batch_size: batch_size) do |batch|
|
|
139
144
|
tokens = batch.select do |token|
|
|
140
145
|
scopes_match?(token.scopes, scopes, application&.scopes) &&
|
|
141
|
-
custom_attributes_match?(token, custom_attributes)
|
|
146
|
+
custom_attributes_match?(token, custom_attributes) &&
|
|
147
|
+
(filter.nil? || filter.call(token))
|
|
142
148
|
end
|
|
143
149
|
|
|
144
150
|
matching_tokens.concat(tokens)
|
|
@@ -147,6 +153,32 @@ module Doorkeeper
|
|
|
147
153
|
matching_tokens.max_by(&:created_at)
|
|
148
154
|
end
|
|
149
155
|
|
|
156
|
+
# Checks whether a candidate token for reuse matches the refresh token
|
|
157
|
+
# requirement of the current request.
|
|
158
|
+
#
|
|
159
|
+
# The candidate's refresh token presence must match what the request asks
|
|
160
|
+
# for, in both directions. A request that expects a refresh token (e.g. an
|
|
161
|
+
# authorization_code or password grant while `use_refresh_token` is
|
|
162
|
+
# enabled) must not reuse a token issued without one, or the response would
|
|
163
|
+
# silently omit the refresh token. Conversely, a request that does not ask
|
|
164
|
+
# for a refresh token must not reuse a token that carries one, or the
|
|
165
|
+
# response would return a refresh token the request never requested (this
|
|
166
|
+
# is reachable when `refresh_token_enabled` is a per-request callable).
|
|
167
|
+
# When no matching token satisfies the requirement a fresh token is
|
|
168
|
+
# issued instead.
|
|
169
|
+
#
|
|
170
|
+
# @param access_token [Doorkeeper::AccessToken]
|
|
171
|
+
# the candidate token for reuse
|
|
172
|
+
# @param token_attributes [Hash]
|
|
173
|
+
# attributes for the token being requested
|
|
174
|
+
#
|
|
175
|
+
# @return [Boolean] true if the candidate token's refresh token presence
|
|
176
|
+
# matches the request's refresh token requirement, false otherwise
|
|
177
|
+
#
|
|
178
|
+
def refresh_token_matches?(access_token, token_attributes)
|
|
179
|
+
access_token.refresh_token.present? == !!token_attributes[:use_refresh_token]
|
|
180
|
+
end
|
|
181
|
+
|
|
150
182
|
# Checks whether the token scopes match the scopes from the parameters
|
|
151
183
|
#
|
|
152
184
|
# @param token_scopes [#to_s]
|
|
@@ -220,10 +252,18 @@ module Doorkeeper
|
|
|
220
252
|
scopes = Doorkeeper::OAuth::Scopes.from_string(scopes) if scopes.is_a?(String)
|
|
221
253
|
|
|
222
254
|
if Doorkeeper.config.reuse_access_token
|
|
223
|
-
|
|
255
|
+
# An empty hash must stay distinct from nil here: nil ignores custom
|
|
256
|
+
# attributes when matching, while an empty hash only matches tokens
|
|
257
|
+
# that have no custom attributes set.
|
|
258
|
+
custom_attributes = extract_custom_attributes(token_attributes)
|
|
259
|
+
# The refresh token requirement participates in the matching itself
|
|
260
|
+
# rather than being checked on its single newest result: the newest
|
|
261
|
+
# matching token may carry the wrong refresh token presence (e.g. it
|
|
262
|
+
# was issued through a grant with a different `use_refresh_token`)
|
|
263
|
+
# while an older token satisfies the request and can still be reused.
|
|
224
264
|
access_token = matching_token_for(
|
|
225
265
|
application, resource_owner, scopes, custom_attributes: custom_attributes, include_expired: false,
|
|
226
|
-
)
|
|
266
|
+
) { |token| refresh_token_matches?(token, token_attributes) }
|
|
227
267
|
|
|
228
268
|
return access_token if access_token&.reusable?
|
|
229
269
|
end
|
|
@@ -5,6 +5,7 @@ module Doorkeeper
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
include OAuth::Helpers
|
|
8
|
+
include Models::Concerns::WriteToPrimary
|
|
8
9
|
include Models::Orderable
|
|
9
10
|
include Models::SecretStorable
|
|
10
11
|
include Models::Scopes
|
|
@@ -27,9 +28,8 @@ module Doorkeeper
|
|
|
27
28
|
app = by_uid(uid)
|
|
28
29
|
return unless app
|
|
29
30
|
return app if secret.blank? && !app.confidential?
|
|
30
|
-
return unless app.secret_matches?(secret)
|
|
31
31
|
|
|
32
|
-
app
|
|
32
|
+
app if app.secret_matches?(secret)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Returns an instance of the Doorkeeper::Application with specific UID.
|
|
@@ -76,18 +76,26 @@ module Doorkeeper
|
|
|
76
76
|
# @return [Boolean] Whether the given secret matches the stored secret
|
|
77
77
|
# of this application.
|
|
78
78
|
#
|
|
79
|
+
# @note When the secret matches only via the fallback strategy, the stored
|
|
80
|
+
# secret is upgraded to the active strategy as a side-effect (mirrors
|
|
81
|
+
# the find_by_plaintext_token -> find_by_fallback_token pattern).
|
|
82
|
+
#
|
|
79
83
|
def secret_matches?(input)
|
|
80
84
|
# return false if either is nil, since secure_compare depends on strings
|
|
81
85
|
# but Application secrets MAY be nil depending on confidentiality.
|
|
82
86
|
return false if input.nil? || secret.nil?
|
|
83
87
|
|
|
88
|
+
input = input.to_s
|
|
89
|
+
|
|
84
90
|
# When matching the secret by comparer function, all is well.
|
|
85
91
|
return true if secret_strategy.secret_matches?(input, secret)
|
|
86
92
|
|
|
87
|
-
# When fallback lookup is enabled, ensure applications
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
# When fallback lookup is enabled, ensure applications with plain secrets
|
|
94
|
+
# can still be found, upgrading the stored secret to the active strategy
|
|
95
|
+
# on a successful match.
|
|
96
|
+
if fallback_secret_strategy&.secret_matches?(input, secret)
|
|
97
|
+
self.class.upgrade_fallback_value(self, :secret, input)
|
|
98
|
+
true
|
|
91
99
|
else
|
|
92
100
|
false
|
|
93
101
|
end
|
|
@@ -84,7 +84,16 @@ module Doorkeeper
|
|
|
84
84
|
#
|
|
85
85
|
def upgrade_fallback_value(instance, attr, plain_secret)
|
|
86
86
|
upgraded = secret_strategy.store_secret(instance, attr, plain_secret)
|
|
87
|
-
|
|
87
|
+
|
|
88
|
+
# The upgrade is a write on what is otherwise a read path (finding a
|
|
89
|
+
# record by its secret), so it must reach the primary database when
|
|
90
|
+
# automatic role switching would route the surrounding request to a
|
|
91
|
+
# read replica.
|
|
92
|
+
if respond_to?(:with_primary_role)
|
|
93
|
+
with_primary_role { instance.update(attr => upgraded) }
|
|
94
|
+
else
|
|
95
|
+
instance.update(attr => upgraded)
|
|
96
|
+
end
|
|
88
97
|
end
|
|
89
98
|
|
|
90
99
|
##
|
|
@@ -10,6 +10,17 @@ module Doorkeeper
|
|
|
10
10
|
def uri_with_query(url, parameters = {})
|
|
11
11
|
uri = URI.parse(url)
|
|
12
12
|
original_query = Rack::Utils.parse_query(uri.query)
|
|
13
|
+
# `parse_query` yields string keys while `parameters` uses symbol
|
|
14
|
+
# keys, so a naive merge cannot dedupe a collision (e.g. a registered
|
|
15
|
+
# redirect_uri already carrying `state`) and would emit the param
|
|
16
|
+
# twice. Normalize keys so the response parameters win over any
|
|
17
|
+
# same-named query already present in the redirect_uri. Blank
|
|
18
|
+
# response parameters are dropped before the merge: they carry no
|
|
19
|
+
# value to respond with, and letting them clobber a same-named
|
|
20
|
+
# registered parameter would violate RFC 6749 §3.1.2 (the registered
|
|
21
|
+
# query component "MUST be retained when adding additional query
|
|
22
|
+
# parameters").
|
|
23
|
+
parameters = parameters.transform_keys(&:to_s).reject { |_, value| value.blank? }
|
|
13
24
|
uri.query = build_query(original_query.merge(parameters))
|
|
14
25
|
uri.to_s
|
|
15
26
|
end
|
|
@@ -13,6 +13,9 @@ module Doorkeeper
|
|
|
13
13
|
attr_reader :grant, :client, :redirect_uri, :access_token, :code_verifier,
|
|
14
14
|
:invalid_request_reason, :missing_param
|
|
15
15
|
|
|
16
|
+
# A scope parameter is deliberately not read here: RFC 6749 does not
|
|
17
|
+
# define one for the authorization_code token request (§4.1.3), so it
|
|
18
|
+
# is ignored and the access token inherits the scopes of the grant.
|
|
16
19
|
def initialize(server, grant, client, parameters = {})
|
|
17
20
|
super()
|
|
18
21
|
@server = server
|
|
@@ -64,7 +67,7 @@ module Doorkeeper
|
|
|
64
67
|
@missing_param =
|
|
65
68
|
if grant&.uses_pkce? && code_verifier.blank?
|
|
66
69
|
:code_verifier
|
|
67
|
-
elsif client &&
|
|
70
|
+
elsif client && Doorkeeper.config.force_pkce? && code_verifier.blank?
|
|
68
71
|
:code_verifier
|
|
69
72
|
elsif redirect_uri.blank?
|
|
70
73
|
:redirect_uri
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# The full registry rather than just credentials: requiring only
|
|
4
|
+
# client_authentication/credentials fires the ClientAuthentication autoload
|
|
5
|
+
# mid-load, which re-requires the in-progress file and warns under -w
|
|
6
|
+
# ("circular require considered harmful").
|
|
7
|
+
require "doorkeeper/client_authentication"
|
|
8
|
+
|
|
3
9
|
module Doorkeeper
|
|
4
10
|
module OAuth
|
|
5
11
|
class Client
|
|
12
|
+
# @deprecated Moved to +Doorkeeper::ClientAuthentication::Credentials+.
|
|
13
|
+
# This alias keeps the long-standing +Doorkeeper::OAuth::Client::Credentials+
|
|
14
|
+
# constant resolvable for one release so referencing code does not raise
|
|
15
|
+
# +NameError+; update references to the new constant. Note the legacy
|
|
16
|
+
# +.from_request+/+.from_basic+/+.from_params+ class methods are gone —
|
|
17
|
+
# client credential extraction now goes through the client authentication
|
|
18
|
+
# registry (RFC 6749 §2.3). Marked with +deprecate_constant+, so Ruby
|
|
19
|
+
# warns on access when deprecation warnings are enabled
|
|
20
|
+
# (+Warning[:deprecated] = true+ or +-W:deprecated+).
|
|
21
|
+
Credentials = Doorkeeper::ClientAuthentication::Credentials
|
|
22
|
+
deprecate_constant :Credentials
|
|
23
|
+
|
|
6
24
|
attr_reader :application
|
|
7
25
|
|
|
8
26
|
delegate :id, :name, :uid, :redirect_uri, :scopes, :confidential, to: :@application
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
module OAuth
|
|
5
|
+
module ClientAuthentication
|
|
6
|
+
# RFC 6749 §2.3.1 "client_secret_basic": client credentials are sent
|
|
7
|
+
# using HTTP Basic authentication.
|
|
8
|
+
#
|
|
9
|
+
# Known deviation: §2.3.1 also requires the client_id and client_secret
|
|
10
|
+
# to be form-urlencoded before being placed into the Basic header.
|
|
11
|
+
# Doorkeeper has never URL-decoded them (like much of the ecosystem),
|
|
12
|
+
# and this strategy deliberately keeps that behaviour — adding the
|
|
13
|
+
# decoding now would break every existing client whose credentials
|
|
14
|
+
# contain URL-encodable characters.
|
|
15
|
+
class ClientSecretBasic
|
|
16
|
+
# Match whenever the header decodes to a non-blank +client_id+ — i.e.
|
|
17
|
+
# whenever a Basic authentication *attempt* is present. The secret may
|
|
18
|
+
# be empty (public clients) or missing; those are still Basic auth
|
|
19
|
+
# attempts and must be claimed here so that invalid credentials fail
|
|
20
|
+
# with +invalid_client+ instead of silently falling through to another
|
|
21
|
+
# configured method or the fallback (which would downgrade a failed
|
|
22
|
+
# authentication attempt to "no authentication provided").
|
|
23
|
+
def self.matches_request?(request)
|
|
24
|
+
credentials_from(request).present?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.authenticate(request)
|
|
28
|
+
client_id, client_secret = credentials_from(request)
|
|
29
|
+
return unless client_id
|
|
30
|
+
|
|
31
|
+
Doorkeeper::ClientAuthentication::Credentials.new(client_id, client_secret)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the decoded [client_id, client_secret] pair, or nil when the
|
|
35
|
+
# request carries no HTTP Basic +client_id+. A header that merely
|
|
36
|
+
# starts with "Basic " but decodes to an empty/blank client_id (e.g.
|
|
37
|
+
# an empty payload or a leading ":") is not a usable attempt and does
|
|
38
|
+
# not match.
|
|
39
|
+
def self.credentials_from(request)
|
|
40
|
+
authorization = request.authorization.to_s
|
|
41
|
+
return unless authorization.downcase.start_with?("basic ")
|
|
42
|
+
|
|
43
|
+
value = authorization.split(" ", 2).last
|
|
44
|
+
client_id, client_secret = Base64.decode64(value.to_s).split(":", 2)
|
|
45
|
+
return if client_id.blank?
|
|
46
|
+
|
|
47
|
+
[client_id, client_secret]
|
|
48
|
+
end
|
|
49
|
+
private_class_method :credentials_from
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
module OAuth
|
|
5
|
+
module ClientAuthentication
|
|
6
|
+
# RFC 6749 §2.3.1 "client_secret_post": client credentials are sent in
|
|
7
|
+
# the request body. The query string is intentionally ignored so that
|
|
8
|
+
# credentials must be supplied in the body as the spec requires.
|
|
9
|
+
class ClientSecretPost
|
|
10
|
+
def self.matches_request?(request)
|
|
11
|
+
params = request.request_parameters.with_indifferent_access
|
|
12
|
+
|
|
13
|
+
request.post? &&
|
|
14
|
+
params[:client_id].present? &&
|
|
15
|
+
params[:client_secret].present?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.authenticate(request)
|
|
19
|
+
params = request.request_parameters.with_indifferent_access
|
|
20
|
+
|
|
21
|
+
Doorkeeper::ClientAuthentication::Credentials.new(
|
|
22
|
+
params[:client_id],
|
|
23
|
+
params[:client_secret],
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
module OAuth
|
|
5
|
+
module ClientAuthentication
|
|
6
|
+
# RFC 6749 §2.3 "none": a public client that authenticates with only a
|
|
7
|
+
# client_id and no secret (in the request body, not the query string).
|
|
8
|
+
class None
|
|
9
|
+
# Requires the Authorization header to be absent or blank: a request
|
|
10
|
+
# that carries a non-blank one (Basic, Bearer, or anything else) is
|
|
11
|
+
# attempting header-based authentication and must not be silently
|
|
12
|
+
# treated as an unauthenticated public client. This is narrower than
|
|
13
|
+
# the legacy +from_params+ extractor, which read the body +client_id+
|
|
14
|
+
# regardless of the Authorization header.
|
|
15
|
+
def self.matches_request?(request)
|
|
16
|
+
params = request.request_parameters.with_indifferent_access
|
|
17
|
+
|
|
18
|
+
request.post? &&
|
|
19
|
+
request.authorization.blank? &&
|
|
20
|
+
params[:client_id].present? &&
|
|
21
|
+
params[:client_secret].blank?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.authenticate(request)
|
|
25
|
+
params = request.request_parameters.with_indifferent_access
|
|
26
|
+
|
|
27
|
+
Doorkeeper::ClientAuthentication::Credentials.new(params[:client_id], nil)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -45,8 +45,11 @@ module Doorkeeper
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def find_active_existing_token_for(client, scopes, attributes)
|
|
48
|
+
# An empty hash must stay distinct from nil here: nil ignores custom
|
|
49
|
+
# attributes when matching, while an empty hash only matches tokens
|
|
50
|
+
# that have no custom attributes set.
|
|
48
51
|
custom_attributes = Doorkeeper.config.access_token_model
|
|
49
|
-
.extract_custom_attributes(attributes)
|
|
52
|
+
.extract_custom_attributes(attributes)
|
|
50
53
|
Doorkeeper.config.access_token_model.matching_token_for(
|
|
51
54
|
client, nil, scopes, custom_attributes: custom_attributes, include_expired: false,
|
|
52
55
|
)
|
|
@@ -28,17 +28,22 @@ module Doorkeeper
|
|
|
28
28
|
token_type: auth.token.token_type,
|
|
29
29
|
expires_in: auth.token.expires_in_seconds,
|
|
30
30
|
state: pre_auth.state,
|
|
31
|
-
}
|
|
31
|
+
}.merge(iss_parameter)
|
|
32
32
|
elsif auth.try(:access_grant?)
|
|
33
33
|
{
|
|
34
34
|
code: auth.token.plaintext_token,
|
|
35
35
|
state: pre_auth.state,
|
|
36
|
-
}
|
|
36
|
+
}.merge(iss_parameter)
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def redirect_uri
|
|
41
41
|
if URIChecker.oob_uri?(pre_auth.redirect_uri)
|
|
42
|
+
# Out-of-band "redirects" render the code/token on an authorization
|
|
43
|
+
# server page for the user to copy manually; there is no redirect
|
|
44
|
+
# back to the client. RFC 9207 scopes the iss parameter to the
|
|
45
|
+
# authorization response sent to the client, so - like state, which
|
|
46
|
+
# oob_redirect also omits - iss is intentionally not carried here.
|
|
42
47
|
auth.oob_redirect
|
|
43
48
|
elsif response_on_fragment
|
|
44
49
|
Authorization::URIBuilder.uri_with_fragment(pre_auth.redirect_uri, body)
|
|
@@ -46,6 +51,15 @@ module Doorkeeper
|
|
|
46
51
|
Authorization::URIBuilder.uri_with_query(pre_auth.redirect_uri, body)
|
|
47
52
|
end
|
|
48
53
|
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# RFC 9207 Authorization Server Issuer Identification: advertise the
|
|
58
|
+
# issuer in the authorization response only when an issuer is configured.
|
|
59
|
+
def iss_parameter
|
|
60
|
+
issuer = Doorkeeper.config.issuer
|
|
61
|
+
issuer.present? ? { iss: issuer } : {}
|
|
62
|
+
end
|
|
49
63
|
end
|
|
50
64
|
end
|
|
51
65
|
end
|