doorkeeper 5.9.7 → 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 -12
- 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/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 +113 -12
- 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/metadata_response.rb +157 -0
- data/lib/doorkeeper/oauth/pre_authorization.rb +12 -5
- data/lib/doorkeeper/oauth/token.rb +0 -76
- 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 +65 -13
- metadata +14 -7
- data/lib/doorkeeper/oauth/client/credentials.rb +0 -86
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)
|
|
@@ -341,6 +381,10 @@ module Doorkeeper
|
|
|
341
381
|
#
|
|
342
382
|
option :realm, default: "Doorkeeper"
|
|
343
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
|
+
|
|
344
388
|
# Forces the usage of the HTTPS protocol in non-native redirect uris
|
|
345
389
|
# (enabled by default in non-development environments). OAuth2
|
|
346
390
|
# delegates security in communication to the HTTPS protocol so it is
|
|
@@ -417,6 +461,10 @@ module Doorkeeper
|
|
|
417
461
|
option :application_class,
|
|
418
462
|
default: "Doorkeeper::Application"
|
|
419
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
|
+
|
|
420
468
|
# Allows to set blank redirect URIs for Applications in case
|
|
421
469
|
# server configured to use URI-less grant flows.
|
|
422
470
|
#
|
|
@@ -600,8 +648,57 @@ module Doorkeeper
|
|
|
600
648
|
pkce_code_challenge_methods
|
|
601
649
|
end
|
|
602
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.
|
|
603
692
|
def client_credentials_methods
|
|
604
|
-
@
|
|
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)
|
|
605
702
|
end
|
|
606
703
|
|
|
607
704
|
def access_token_methods
|
|
@@ -669,13 +766,17 @@ module Doorkeeper
|
|
|
669
766
|
def calculate_token_grant_types
|
|
670
767
|
types = grant_flows - ["implicit"]
|
|
671
768
|
types << "refresh_token" if refresh_token_enabled?
|
|
672
|
-
types
|
|
769
|
+
types.uniq
|
|
673
770
|
end
|
|
674
771
|
|
|
675
772
|
# Calculates grant flows configured by the user in Doorkeeper
|
|
676
773
|
# configuration considering registered aliases that is exposed
|
|
677
774
|
# to single or multiple other flows.
|
|
678
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
|
+
#
|
|
679
780
|
def calculate_grant_flows
|
|
680
781
|
configured_flows = grant_flows.map(&:to_s)
|
|
681
782
|
aliases = Doorkeeper::GrantFlow.aliases.keys.map(&:to_s)
|
|
@@ -687,6 +788,8 @@ module Doorkeeper
|
|
|
687
788
|
flows.concat(Doorkeeper::GrantFlow.expand_alias(flow_alias))
|
|
688
789
|
end
|
|
689
790
|
|
|
791
|
+
flows << "refresh_token" if refresh_token_enabled?
|
|
792
|
+
|
|
690
793
|
flows.flatten.uniq
|
|
691
794
|
end
|
|
692
795
|
|
|
@@ -717,9 +820,7 @@ module Doorkeeper
|
|
|
717
820
|
end
|
|
718
821
|
|
|
719
822
|
def calculate_token_grant_flows
|
|
720
|
-
|
|
721
|
-
flows << Doorkeeper::GrantFlow.get("refresh_token") if refresh_token_enabled?
|
|
722
|
-
flows
|
|
823
|
+
enabled_grant_flows.select(&:handles_grant_type?)
|
|
723
824
|
end
|
|
724
825
|
end
|
|
725
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -5,7 +5,7 @@ module Doorkeeper
|
|
|
5
5
|
class ErrorResponse < BaseResponse
|
|
6
6
|
include OAuth::Helpers
|
|
7
7
|
|
|
8
|
-
NON_REDIRECTABLE_STATES = %i[invalid_redirect_uri invalid_client
|
|
8
|
+
NON_REDIRECTABLE_STATES = %i[invalid_redirect_uri invalid_client].freeze
|
|
9
9
|
|
|
10
10
|
def self.from_request(request, attributes = {})
|
|
11
11
|
new(
|
|
@@ -38,6 +38,7 @@ module Doorkeeper
|
|
|
38
38
|
@exception_class = attributes[:exception_class]
|
|
39
39
|
@redirect_uri = attributes[:redirect_uri]
|
|
40
40
|
@response_on_fragment = attributes[:response_on_fragment]
|
|
41
|
+
@issuer = attributes[:issuer]
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
def body
|
|
@@ -45,11 +46,19 @@ module Doorkeeper
|
|
|
45
46
|
error: name,
|
|
46
47
|
error_description: description,
|
|
47
48
|
state: state,
|
|
49
|
+
# RFC 9207 scopes the issuer to authorization error responses sent to
|
|
50
|
+
# the client, i.e. the ones redirected back to its redirect_uri. It is
|
|
51
|
+
# supplied only by the authorization endpoint (never token,
|
|
52
|
+
# introspection or protected resource), and gated on redirectable? so
|
|
53
|
+
# non-redirectable errors (invalid_client, invalid_redirect_uri) and
|
|
54
|
+
# out-of-band flows - which render to the user instead of redirecting
|
|
55
|
+
# to the client - do not carry it. Blank values are dropped below.
|
|
56
|
+
iss: (@issuer if redirectable?),
|
|
48
57
|
}.reject { |_, v| v.blank? }
|
|
49
58
|
end
|
|
50
59
|
|
|
51
60
|
def status
|
|
52
|
-
if name == :invalid_client
|
|
61
|
+
if name == :invalid_client
|
|
53
62
|
:unauthorized
|
|
54
63
|
else
|
|
55
64
|
:bad_request
|