doorkeeper 5.9.9 → 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 +44 -22
- data/README.md +1 -0
- data/app/controllers/doorkeeper/authorizations_controller.rb +3 -33
- data/app/controllers/doorkeeper/authorized_applications_controller.rb +0 -23
- 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/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 -15
- data/lib/doorkeeper/errors.rb +1 -0
- data/lib/doorkeeper/models/access_token_mixin.rb +40 -5
- 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/code_response.rb +16 -2
- data/lib/doorkeeper/oauth/error_response.rb +11 -2
- data/lib/doorkeeper/oauth/helpers/uri_checker.rb +0 -14
- data/lib/doorkeeper/oauth/metadata_response.rb +157 -0
- data/lib/doorkeeper/oauth/pre_authorization.rb +12 -32
- data/lib/doorkeeper/oauth/token.rb +2 -95
- data/lib/doorkeeper/oauth/token_introspection.rb +27 -7
- data/lib/doorkeeper/orm/active_record/mixins/application.rb +2 -2
- data/lib/doorkeeper/orm/active_record/redirect_uri_validator.rb +1 -5
- data/lib/doorkeeper/rails/routes/mapping.rb +1 -0
- data/lib/doorkeeper/rails/routes.rb +6 -0
- data/lib/doorkeeper/request.rb +65 -18
- 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 +67 -18
- metadata +14 -7
- data/lib/doorkeeper/oauth/client/credentials.rb +0 -86
|
@@ -8,14 +8,68 @@ module Doorkeeper
|
|
|
8
8
|
# Validates configuration options to be set properly.
|
|
9
9
|
#
|
|
10
10
|
def validate!
|
|
11
|
+
validate_client_authentication_conflict
|
|
12
|
+
validate_client_authentication_registered
|
|
11
13
|
validate_reuse_access_token_value
|
|
12
14
|
validate_token_reuse_limit
|
|
13
15
|
validate_secret_strategies
|
|
14
16
|
validate_pkce_code_challenge_methods
|
|
17
|
+
validate_custom_metadata
|
|
18
|
+
validate_refresh_token_flow
|
|
19
|
+
validate_issuer_format
|
|
20
|
+
validate_issuer_metadata_discoverability
|
|
15
21
|
end
|
|
16
22
|
|
|
17
23
|
private
|
|
18
24
|
|
|
25
|
+
# Warn once, at configuration time, when both the deprecated
|
|
26
|
+
# +client_credentials+ and the new +client_authentication+ options are
|
|
27
|
+
# set. +client_authentication+ takes precedence; the warning lives here
|
|
28
|
+
# (rather than in the memoised resolver) so it is not swallowed and
|
|
29
|
+
# surfaces during boot instead of on the first request.
|
|
30
|
+
def validate_client_authentication_conflict
|
|
31
|
+
return unless instance_variable_defined?(:@client_credentials_methods) &&
|
|
32
|
+
instance_variable_defined?(:@client_authentication)
|
|
33
|
+
|
|
34
|
+
::Rails.logger.warn(
|
|
35
|
+
"[DOORKEEPER] Both client_credentials and client_authentication are set, " \
|
|
36
|
+
"using client_authentication",
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Warn about configured client authentication methods that are not
|
|
41
|
+
# registered (e.g. a typo, or an extension that failed to load). Such
|
|
42
|
+
# names are silently ignored when resolving the methods, which could
|
|
43
|
+
# otherwise leave the application with no usable authentication methods.
|
|
44
|
+
def validate_client_authentication_registered
|
|
45
|
+
# The deprecated client_credentials path already validates its own input.
|
|
46
|
+
return if instance_variable_defined?(:@client_credentials_methods) &&
|
|
47
|
+
!instance_variable_defined?(:@client_authentication)
|
|
48
|
+
|
|
49
|
+
configured = client_authentication
|
|
50
|
+
unknown = configured.reject { |name| Doorkeeper::ClientAuthentication.get(name) }
|
|
51
|
+
|
|
52
|
+
unless unknown.empty?
|
|
53
|
+
::Rails.logger.warn(
|
|
54
|
+
"[DOORKEEPER] Unknown client authentication method(s) configured and will be ignored: " \
|
|
55
|
+
"#{unknown.map(&:inspect).join(", ")}. " \
|
|
56
|
+
"Ensure each method is registered (e.g. by the extension that provides it).",
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A configuration that resolves to zero usable methods fails every
|
|
61
|
+
# client authentication attempt (all requests fall back to no
|
|
62
|
+
# credentials), so make that loud rather than silent.
|
|
63
|
+
return unless (configured - unknown).empty?
|
|
64
|
+
|
|
65
|
+
::Rails.logger.error(
|
|
66
|
+
"[DOORKEEPER] No usable client authentication methods are configured " \
|
|
67
|
+
"(client_authentication resolved to an empty set). All client authentication " \
|
|
68
|
+
"will fail. Configure at least one registered method, e.g. " \
|
|
69
|
+
"client_authentication #{Doorkeeper::ClientAuthentication::DEFAULT_METHODS.inspect}.",
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
19
73
|
# Determine whether +reuse_access_token+ and a non-restorable
|
|
20
74
|
# +token_secret_strategy+ have both been activated.
|
|
21
75
|
#
|
|
@@ -51,7 +105,15 @@ module Doorkeeper
|
|
|
51
105
|
end
|
|
52
106
|
|
|
53
107
|
def validate_pkce_code_challenge_methods
|
|
54
|
-
|
|
108
|
+
methods = pkce_code_challenge_methods.map(&:to_s)
|
|
109
|
+
if methods.all? { |method| method.match?(/\A(?:plain|S256)\z/) }
|
|
110
|
+
# Persist the normalized (string) values only when the option was
|
|
111
|
+
# explicitly configured — the default is already normalized, and an
|
|
112
|
+
# unconfigured option should stay undefined rather than have
|
|
113
|
+
# validation flip its instance_variable_defined? signal.
|
|
114
|
+
@pkce_code_challenge_methods = methods if instance_variable_defined?(:@pkce_code_challenge_methods)
|
|
115
|
+
return
|
|
116
|
+
end
|
|
55
117
|
|
|
56
118
|
::Rails.logger.warn(
|
|
57
119
|
"[DOORKEEPER] You have configured an invalid value for pkce_code_challenge_methods option. " \
|
|
@@ -60,6 +122,105 @@ module Doorkeeper
|
|
|
60
122
|
|
|
61
123
|
@pkce_code_challenge_methods = ["plain", "S256"]
|
|
62
124
|
end
|
|
125
|
+
|
|
126
|
+
def validate_custom_metadata
|
|
127
|
+
return if custom_metadata.is_a? Hash
|
|
128
|
+
|
|
129
|
+
::Rails.logger.warn(
|
|
130
|
+
"[DOORKEEPER] You have configured an invalid value for custom_metadata option. " \
|
|
131
|
+
"It must be a Hash, and will be overridden with an empty hash.",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@custom_metadata = {}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Warn when the refresh_token grant flow is enabled but refresh tokens
|
|
138
|
+
# are never issued: refresh token requests would always fail because
|
|
139
|
+
# there are no tokens to refresh. The flow is enabled automatically
|
|
140
|
+
# when +use_refresh_token+ is configured, so it doesn't need to be
|
|
141
|
+
# listed in +grant_flows+ explicitly.
|
|
142
|
+
#
|
|
143
|
+
# +calculate_grant_flows+ is used (rather than the raw +grant_flows+) so
|
|
144
|
+
# that the flow is also detected when enabled through a registered
|
|
145
|
+
# grant-flow alias. When +use_refresh_token+ is not configured the
|
|
146
|
+
# refresh_token flow is not appended implicitly, so its presence there
|
|
147
|
+
# means it was requested explicitly (directly or via an alias).
|
|
148
|
+
def validate_refresh_token_flow
|
|
149
|
+
return if refresh_token_enabled?
|
|
150
|
+
return unless calculate_grant_flows.map(&:to_s).include?("refresh_token")
|
|
151
|
+
|
|
152
|
+
::Rails.logger.warn(
|
|
153
|
+
"[DOORKEEPER] You have enabled the refresh_token grant flow without " \
|
|
154
|
+
"configuring use_refresh_token, so refresh tokens will not be issued. " \
|
|
155
|
+
"Configure use_refresh_token to issue refresh tokens (the refresh_token " \
|
|
156
|
+
"grant flow is then enabled automatically).",
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Warn when a configured issuer is not RFC-compliant. RFC 8414 (the
|
|
161
|
+
# metadata issuer) and RFC 9207 (the authorization response iss parameter)
|
|
162
|
+
# both require an https URL with a host and no query or fragment component.
|
|
163
|
+
# The value is still used as-is - this is a warning, not a hard failure, so
|
|
164
|
+
# local setups using e.g. http://localhost keep working - but a
|
|
165
|
+
# non-compliant issuer produces responses that strict clients may reject.
|
|
166
|
+
def validate_issuer_format
|
|
167
|
+
return if issuer.blank?
|
|
168
|
+
|
|
169
|
+
uri =
|
|
170
|
+
begin
|
|
171
|
+
URI.parse(issuer.to_s)
|
|
172
|
+
rescue URI::InvalidURIError
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
return if uri.is_a?(URI::HTTPS) && uri.host.present? &&
|
|
177
|
+
uri.query.nil? && uri.fragment.nil?
|
|
178
|
+
|
|
179
|
+
::Rails.logger.warn(
|
|
180
|
+
"[DOORKEEPER] issuer #{redacted_issuer.inspect} is not RFC-compliant: " \
|
|
181
|
+
"RFC 8414 and RFC 9207 require an https URL with a host and no query " \
|
|
182
|
+
"or fragment component. It is still advertised in the metadata and " \
|
|
183
|
+
"emitted as the iss parameter as-is, but strict clients may reject it.",
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Warn when a path-bearing issuer cannot be discovered. RFC 8414 clients
|
|
188
|
+
# build the metadata URL by inserting the well-known path into the issuer,
|
|
189
|
+
# so an issuer of https://host/tenant is looked up at
|
|
190
|
+
# https://host/.well-known/oauth-authorization-server/tenant. Doorkeeper
|
|
191
|
+
# only serves the document at the root well-known path, so such a value is
|
|
192
|
+
# not discoverable (and its advertised issuer would not match the lookup).
|
|
193
|
+
# This is a separate concern from validate_issuer_format: a path component
|
|
194
|
+
# is valid per RFC 8414, just unsupported by Doorkeeper's fixed route.
|
|
195
|
+
def validate_issuer_metadata_discoverability
|
|
196
|
+
return if issuer.blank?
|
|
197
|
+
|
|
198
|
+
uri =
|
|
199
|
+
begin
|
|
200
|
+
URI.parse(issuer.to_s)
|
|
201
|
+
rescue URI::InvalidURIError
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
return if uri&.host.blank?
|
|
206
|
+
return if uri.path.blank? || uri.path == "/"
|
|
207
|
+
|
|
208
|
+
::Rails.logger.warn(
|
|
209
|
+
"[DOORKEEPER] issuer #{redacted_issuer.inspect} has a path component, but " \
|
|
210
|
+
"Doorkeeper serves its RFC 8414 metadata only at the root " \
|
|
211
|
+
"/.well-known/oauth-authorization-server. RFC 8414 clients derive the " \
|
|
212
|
+
"metadata URL from the issuer path (…/.well-known/" \
|
|
213
|
+
"oauth-authorization-server#{uri.path}), so they will not discover the " \
|
|
214
|
+
"document. Use a host-only issuer, or route the derived well-known path " \
|
|
215
|
+
"to Doorkeeper.",
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Redact any userinfo (e.g. a misconfigured user:pass@host) before the
|
|
220
|
+
# issuer is written to the log, so credentials are not leaked there.
|
|
221
|
+
def redacted_issuer
|
|
222
|
+
issuer.to_s.sub(%r{//[^/@]*@}, "//***@")
|
|
223
|
+
end
|
|
63
224
|
end
|
|
64
225
|
end
|
|
65
226
|
end
|
data/lib/doorkeeper/config.rb
CHANGED
|
@@ -65,17 +65,50 @@ module Doorkeeper
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
# Change the way client credentials are retrieved from the request object.
|
|
68
|
-
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
|
69
|
-
# falls back to the `:client_id` and `:client_secret` params from the
|
|
70
|
-
# `params` object.
|
|
71
68
|
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
# +
|
|
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.
|
|
75
78
|
#
|
|
76
79
|
# @param methods [Array] Define client credentials
|
|
77
80
|
def client_credentials(*methods)
|
|
78
|
-
|
|
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)
|
|
79
112
|
end
|
|
80
113
|
|
|
81
114
|
# Change the way access token is authenticated from the request object.
|
|
@@ -206,6 +239,13 @@ module Doorkeeper
|
|
|
206
239
|
|
|
207
240
|
private
|
|
208
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
|
+
|
|
209
249
|
# Configure the secret storing functionality
|
|
210
250
|
def configure_secrets_for(type, using:, fallback:)
|
|
211
251
|
raise ArgumentError, "Invalid type #{type}" if %i[application token].exclude?(type)
|
|
@@ -329,9 +369,7 @@ module Doorkeeper
|
|
|
329
369
|
option :allow_grant_flow_for_client, default: ->(_grant_flow, _client) { true }
|
|
330
370
|
|
|
331
371
|
# Allows to forbid specific Application redirect URI's by custom rules.
|
|
332
|
-
# Doesn't forbid any URI by default.
|
|
333
|
-
# (`javascript`, `vbscript`, `data`) are always rejected, regardless of
|
|
334
|
-
# this option.
|
|
372
|
+
# Doesn't forbid any URI by default.
|
|
335
373
|
#
|
|
336
374
|
# @param forbid_redirect_uri [Proc] Block or any object respond to #call
|
|
337
375
|
#
|
|
@@ -343,6 +381,10 @@ module Doorkeeper
|
|
|
343
381
|
#
|
|
344
382
|
option :realm, default: "Doorkeeper"
|
|
345
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
|
+
|
|
346
388
|
# Forces the usage of the HTTPS protocol in non-native redirect uris
|
|
347
389
|
# (enabled by default in non-development environments). OAuth2
|
|
348
390
|
# delegates security in communication to the HTTPS protocol so it is
|
|
@@ -419,6 +461,10 @@ module Doorkeeper
|
|
|
419
461
|
option :application_class,
|
|
420
462
|
default: "Doorkeeper::Application"
|
|
421
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
|
+
|
|
422
468
|
# Allows to set blank redirect URIs for Applications in case
|
|
423
469
|
# server configured to use URI-less grant flows.
|
|
424
470
|
#
|
|
@@ -602,8 +648,57 @@ module Doorkeeper
|
|
|
602
648
|
pkce_code_challenge_methods
|
|
603
649
|
end
|
|
604
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.
|
|
605
692
|
def client_credentials_methods
|
|
606
|
-
@
|
|
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)
|
|
607
702
|
end
|
|
608
703
|
|
|
609
704
|
def access_token_methods
|
|
@@ -671,13 +766,17 @@ module Doorkeeper
|
|
|
671
766
|
def calculate_token_grant_types
|
|
672
767
|
types = grant_flows - ["implicit"]
|
|
673
768
|
types << "refresh_token" if refresh_token_enabled?
|
|
674
|
-
types
|
|
769
|
+
types.uniq
|
|
675
770
|
end
|
|
676
771
|
|
|
677
772
|
# Calculates grant flows configured by the user in Doorkeeper
|
|
678
773
|
# configuration considering registered aliases that is exposed
|
|
679
774
|
# to single or multiple other flows.
|
|
680
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
|
+
#
|
|
681
780
|
def calculate_grant_flows
|
|
682
781
|
configured_flows = grant_flows.map(&:to_s)
|
|
683
782
|
aliases = Doorkeeper::GrantFlow.aliases.keys.map(&:to_s)
|
|
@@ -689,6 +788,8 @@ module Doorkeeper
|
|
|
689
788
|
flows.concat(Doorkeeper::GrantFlow.expand_alias(flow_alias))
|
|
690
789
|
end
|
|
691
790
|
|
|
791
|
+
flows << "refresh_token" if refresh_token_enabled?
|
|
792
|
+
|
|
692
793
|
flows.flatten.uniq
|
|
693
794
|
end
|
|
694
795
|
|
|
@@ -719,9 +820,7 @@ module Doorkeeper
|
|
|
719
820
|
end
|
|
720
821
|
|
|
721
822
|
def calculate_token_grant_flows
|
|
722
|
-
|
|
723
|
-
flows << Doorkeeper::GrantFlow.get("refresh_token") if refresh_token_enabled?
|
|
724
|
-
flows
|
|
823
|
+
enabled_grant_flows.select(&:handles_grant_type?)
|
|
725
824
|
end
|
|
726
825
|
end
|
|
727
826
|
end
|
data/lib/doorkeeper/errors.rb
CHANGED
|
@@ -75,6 +75,7 @@ module Doorkeeper
|
|
|
75
75
|
UnableToGenerateToken = Class.new(DoorkeeperError)
|
|
76
76
|
TokenGeneratorNotFound = Class.new(DoorkeeperError)
|
|
77
77
|
NoOrmCleaner = Class.new(DoorkeeperError)
|
|
78
|
+
MissingConfigurationBuilderClass = Class.new(DoorkeeperError)
|
|
78
79
|
|
|
79
80
|
InvalidRequest = Class.new(BaseResponseError)
|
|
80
81
|
InvalidToken = Class.new(BaseResponseError)
|
|
@@ -92,13 +92,16 @@ module Doorkeeper
|
|
|
92
92
|
# A nil value will ignore custom attributes, while an empty hash will
|
|
93
93
|
# only match tokens that have no custom attributes set.
|
|
94
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).
|
|
97
|
+
#
|
|
95
98
|
# @return [Doorkeeper::AccessToken, nil] Access Token instance or
|
|
96
99
|
# nil if matching record was not found
|
|
97
100
|
#
|
|
98
|
-
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)
|
|
99
102
|
tokens = authorized_tokens_for(application&.id, resource_owner)
|
|
100
103
|
tokens = tokens.not_expired unless include_expired
|
|
101
|
-
find_matching_token(tokens, application, custom_attributes, scopes)
|
|
104
|
+
find_matching_token(tokens, application, custom_attributes, scopes, &filter)
|
|
102
105
|
end
|
|
103
106
|
|
|
104
107
|
# Interface to enumerate access token records in batches in order not
|
|
@@ -131,7 +134,7 @@ module Doorkeeper
|
|
|
131
134
|
# @return [Doorkeeper::AccessToken, nil] Access Token instance or
|
|
132
135
|
# nil if matching record was not found
|
|
133
136
|
#
|
|
134
|
-
def find_matching_token(relation, application, custom_attributes, scopes)
|
|
137
|
+
def find_matching_token(relation, application, custom_attributes, scopes, &filter)
|
|
135
138
|
return nil unless relation
|
|
136
139
|
|
|
137
140
|
matching_tokens = []
|
|
@@ -140,7 +143,8 @@ module Doorkeeper
|
|
|
140
143
|
find_access_token_in_batches(relation, batch_size: batch_size) do |batch|
|
|
141
144
|
tokens = batch.select do |token|
|
|
142
145
|
scopes_match?(token.scopes, scopes, application&.scopes) &&
|
|
143
|
-
custom_attributes_match?(token, custom_attributes)
|
|
146
|
+
custom_attributes_match?(token, custom_attributes) &&
|
|
147
|
+
(filter.nil? || filter.call(token))
|
|
144
148
|
end
|
|
145
149
|
|
|
146
150
|
matching_tokens.concat(tokens)
|
|
@@ -149,6 +153,32 @@ module Doorkeeper
|
|
|
149
153
|
matching_tokens.max_by(&:created_at)
|
|
150
154
|
end
|
|
151
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
|
+
|
|
152
182
|
# Checks whether the token scopes match the scopes from the parameters
|
|
153
183
|
#
|
|
154
184
|
# @param token_scopes [#to_s]
|
|
@@ -226,9 +256,14 @@ module Doorkeeper
|
|
|
226
256
|
# attributes when matching, while an empty hash only matches tokens
|
|
227
257
|
# that have no custom attributes set.
|
|
228
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.
|
|
229
264
|
access_token = matching_token_for(
|
|
230
265
|
application, resource_owner, scopes, custom_attributes: custom_attributes, include_expired: false,
|
|
231
|
-
)
|
|
266
|
+
) { |token| refresh_token_matches?(token, token_attributes) }
|
|
232
267
|
|
|
233
268
|
return access_token if access_token&.reusable?
|
|
234
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
|