doorkeeper 6.0.0.beta2 → 6.0.0.rc2
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 +48 -2
- data/README.md +29 -0
- data/app/controllers/doorkeeper/authorizations_controller.rb +66 -3
- data/app/controllers/doorkeeper/authorized_applications_controller.rb +23 -0
- data/app/controllers/doorkeeper/token_info_controller.rb +13 -1
- data/config/locales/en.yml +1 -0
- data/lib/doorkeeper/config/validations.rb +91 -0
- data/lib/doorkeeper/config.rb +18 -2
- data/lib/doorkeeper/errors.rb +14 -0
- data/lib/doorkeeper/grape/helpers.rb +11 -1
- data/lib/doorkeeper/helpers/controller.rb +12 -0
- data/lib/doorkeeper/http_fetcher.rb +79 -19
- data/lib/doorkeeper/models/access_token_mixin.rb +120 -2
- data/lib/doorkeeper/models/concerns/secret_storable.rb +72 -6
- data/lib/doorkeeper/oauth/authorization/token.rb +87 -8
- data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +13 -0
- data/lib/doorkeeper/oauth/client_authentication/private_key_jwt.rb +27 -20
- data/lib/doorkeeper/oauth/client_credentials/creator.rb +17 -3
- data/lib/doorkeeper/oauth/helpers/uri_checker.rb +14 -0
- data/lib/doorkeeper/oauth/metadata_response.rb +4 -3
- data/lib/doorkeeper/oauth/pre_authorization.rb +50 -0
- data/lib/doorkeeper/oauth/refresh_token_request.rb +81 -24
- data/lib/doorkeeper/oauth/resource_indicator_validator.rb +10 -0
- data/lib/doorkeeper/oauth/token.rb +94 -2
- data/lib/doorkeeper/oauth/token_introspection.rb +14 -1
- data/lib/doorkeeper/orm/active_record/mixins/access_grant.rb +1 -0
- data/lib/doorkeeper/orm/active_record/mixins/access_token.rb +1 -0
- data/lib/doorkeeper/orm/active_record/mixins/application.rb +1 -0
- data/lib/doorkeeper/orm/active_record/mixins/secret_storable.rb +113 -0
- data/lib/doorkeeper/orm/active_record/redirect_uri_validator.rb +5 -1
- data/lib/doorkeeper/orm/active_record.rb +1 -0
- data/lib/doorkeeper/rails/helpers.rb +25 -2
- data/lib/doorkeeper/request.rb +19 -7
- data/lib/doorkeeper/version.rb +1 -1
- data/lib/doorkeeper.rb +11 -0
- data/lib/generators/doorkeeper/refresh_token_scopes_generator.rb +43 -0
- data/lib/generators/doorkeeper/templates/add_refresh_token_scopes_to_access_tokens.rb.erb +10 -0
- data/lib/generators/doorkeeper/templates/initializer.rb +94 -15
- data/lib/generators/doorkeeper/templates/migration.rb.erb +9 -0
- metadata +12 -12
|
@@ -13,9 +13,8 @@ module Doorkeeper
|
|
|
13
13
|
existing_token = find_revocable_token_for(client, scopes, attributes)
|
|
14
14
|
|
|
15
15
|
with_revocation(existing_token: existing_token) do
|
|
16
|
-
application = client.is_a?(Doorkeeper.config.application_model) ? client : client&.application
|
|
17
16
|
Doorkeeper.config.access_token_model.create_for(
|
|
18
|
-
application:
|
|
17
|
+
application: application_for(client),
|
|
19
18
|
resource_owner: nil,
|
|
20
19
|
scopes: scopes,
|
|
21
20
|
**attributes,
|
|
@@ -39,10 +38,25 @@ module Doorkeeper
|
|
|
39
38
|
end
|
|
40
39
|
end
|
|
41
40
|
|
|
41
|
+
# A token that would outlive +public_client_access_token_expires_in+
|
|
42
|
+
# is not reused, so the cap holds for a public client whatever tokens
|
|
43
|
+
# it was issued before; a new, capped token is issued instead.
|
|
44
|
+
#
|
|
45
|
+
# The cap is checked on the token found rather than inside the lookup
|
|
46
|
+
# +find_revocable_token_for+ shares: there, an over-cap token must
|
|
47
|
+
# still be found, so that +revoke_previous_client_credentials_token+
|
|
48
|
+
# revokes it when the capped token replaces it.
|
|
42
49
|
def find_reusable_token_for(client, scopes, attributes)
|
|
43
50
|
token = find_active_existing_token_for(client, scopes, attributes)
|
|
51
|
+
return unless token&.reusable?
|
|
44
52
|
|
|
45
|
-
token if
|
|
53
|
+
token if Authorization::Token.within_public_client_expires_in?(
|
|
54
|
+
Doorkeeper.config, application_for(client), token,
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def application_for(client)
|
|
59
|
+
client.is_a?(Doorkeeper.config.application_model) ? client : client&.application
|
|
46
60
|
end
|
|
47
61
|
|
|
48
62
|
def find_revocable_token_for(client, scopes, attributes)
|
|
@@ -6,6 +6,15 @@ module Doorkeeper
|
|
|
6
6
|
module OAuth
|
|
7
7
|
module Helpers
|
|
8
8
|
module URIChecker
|
|
9
|
+
# Schemes a user agent evaluates in the document that navigates to
|
|
10
|
+
# them instead of fetching a resource from somewhere else. With
|
|
11
|
+
# `response_mode=form_post` that document is the authorization
|
|
12
|
+
# server's own page, so a redirect URI with one of these schemes would
|
|
13
|
+
# run the client's script on the server's origin. None of them can be
|
|
14
|
+
# a legitimate redirection endpoint (RFC 6749 Section 3.1.2), so they
|
|
15
|
+
# are refused regardless of the `forbid_redirect_uri` configuration.
|
|
16
|
+
SCRIPT_SCHEMES = %w[javascript vbscript data].freeze
|
|
17
|
+
|
|
9
18
|
def self.valid?(url)
|
|
10
19
|
return true if oob_uri?(url)
|
|
11
20
|
|
|
@@ -74,10 +83,15 @@ module Doorkeeper
|
|
|
74
83
|
|
|
75
84
|
def self.valid_scheme?(uri)
|
|
76
85
|
return false if uri.scheme.blank?
|
|
86
|
+
return false if script_scheme?(uri)
|
|
77
87
|
|
|
78
88
|
%w[localhost].exclude?(uri.scheme)
|
|
79
89
|
end
|
|
80
90
|
|
|
91
|
+
def self.script_scheme?(uri)
|
|
92
|
+
SCRIPT_SCHEMES.include?(uri.scheme.to_s.downcase)
|
|
93
|
+
end
|
|
94
|
+
|
|
81
95
|
def self.hypertext_scheme?(uri)
|
|
82
96
|
%w[http https].include?(uri.scheme)
|
|
83
97
|
end
|
|
@@ -155,10 +155,11 @@ module Doorkeeper
|
|
|
155
155
|
config.pkce_code_challenge_methods_supported
|
|
156
156
|
end
|
|
157
157
|
|
|
158
|
+
# The same predicate the endpoints gate on, so the document cannot
|
|
159
|
+
# advertise a `resource` parameter they would refuse, or stay silent
|
|
160
|
+
# about one they honour.
|
|
158
161
|
def resource_indicators_supported?
|
|
159
|
-
config.resource_indicator_validator.present? &&
|
|
160
|
-
Doorkeeper.config.access_grant_model.resource_indicators_supported? &&
|
|
161
|
-
Doorkeeper.config.access_token_model.resource_indicators_supported?
|
|
162
|
+
config.resource_indicator_validator.present? && ResourceIndicatorValidator.storage_ready?
|
|
162
163
|
end
|
|
163
164
|
end
|
|
164
165
|
end
|
|
@@ -5,6 +5,16 @@ module Doorkeeper
|
|
|
5
5
|
class PreAuthorization
|
|
6
6
|
include Validations
|
|
7
7
|
|
|
8
|
+
# The validations that identify the client and its redirect URI. None of
|
|
9
|
+
# them depend on the resource owner, so the authorization endpoint can
|
|
10
|
+
# run them before authenticating anyone and spare the user a login that
|
|
11
|
+
# can only end on an error page. RFC 6749 Section 4.1.2.1 (Section 4.2.2.1
|
|
12
|
+
# for the implicit flow) asks for the resource owner to be informed when
|
|
13
|
+
# the client_id is missing or invalid, and Section 3.1.2.4 asks the same
|
|
14
|
+
# for the redirect URI; running these first is what lets the endpoint
|
|
15
|
+
# inform them without a detour through the login form.
|
|
16
|
+
CLIENT_VALIDATIONS = %i[client_id client redirect_uri].freeze
|
|
17
|
+
|
|
8
18
|
validate :client_id, error: Errors::InvalidRequest
|
|
9
19
|
validate :client, error: Errors::InvalidClient
|
|
10
20
|
validate :redirect_uri, error: Errors::InvalidRedirectUri
|
|
@@ -17,6 +27,9 @@ module Doorkeeper
|
|
|
17
27
|
validate :code_challenge, error: Errors::InvalidRequest
|
|
18
28
|
validate :code_challenge_method, error: Errors::InvalidCodeChallengeMethod
|
|
19
29
|
validate :resource_indicators, error: Errors::InvalidTarget
|
|
30
|
+
# Runs after :resource_indicators so a malformed target is still answered
|
|
31
|
+
# as the client's error rather than the server's.
|
|
32
|
+
validate :resource_indicator_storage, error: Errors::ServerError
|
|
20
33
|
|
|
21
34
|
attr_reader :client, :code_challenge, :code_challenge_method, :missing_param,
|
|
22
35
|
:redirect_uri, :resource_owner, :response_type, :state,
|
|
@@ -42,6 +55,24 @@ module Doorkeeper
|
|
|
42
55
|
valid?
|
|
43
56
|
end
|
|
44
57
|
|
|
58
|
+
# Runs only CLIENT_VALIDATIONS, in declared order, so a request
|
|
59
|
+
# from an unknown client or with an invalid redirect URI can be refused
|
|
60
|
+
# without a resource owner. Error precedence matches a full #validate
|
|
61
|
+
# run because these are the first validations declared.
|
|
62
|
+
def client_valid?
|
|
63
|
+
@error = nil
|
|
64
|
+
@missing_param = nil
|
|
65
|
+
|
|
66
|
+
self.class.validations.each do |validation|
|
|
67
|
+
next unless CLIENT_VALIDATIONS.include?(validation[:attribute])
|
|
68
|
+
|
|
69
|
+
@error = validation[:options][:error] unless send("validate_#{validation[:attribute]}")
|
|
70
|
+
break if @error
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@error.nil?
|
|
74
|
+
end
|
|
75
|
+
|
|
45
76
|
def scopes
|
|
46
77
|
Scopes.from_string(scope)
|
|
47
78
|
end
|
|
@@ -207,6 +238,25 @@ module Doorkeeper
|
|
|
207
238
|
false
|
|
208
239
|
end
|
|
209
240
|
|
|
241
|
+
# A client asking for a resource this server cannot record the audience
|
|
242
|
+
# of is a misconfiguration — resource_indicator_validator is set, but the
|
|
243
|
+
# `resource` column the doorkeeper:resource_indicators generator adds is
|
|
244
|
+
# not there. Issuing the grant would raise MissingResourceColumn out of
|
|
245
|
+
# Authorization::Code, which the authorization endpoint does not rescue,
|
|
246
|
+
# so the misconfiguration reached the client as a 500. Answering
|
|
247
|
+
# server_error here instead matches what the token endpoint already makes
|
|
248
|
+
# of the same exception, and RFC 6749 Section 4.1.2.1 lists server_error
|
|
249
|
+
# among the errors returned through the redirection URI.
|
|
250
|
+
#
|
|
251
|
+
# Only requests that actually ask for a resource are refused: without the
|
|
252
|
+
# parameter there is nothing to record, and a misconfigured server would
|
|
253
|
+
# otherwise stop authorizing anyone at all.
|
|
254
|
+
def validate_resource_indicator_storage
|
|
255
|
+
return true if @resource_indicators.blank?
|
|
256
|
+
|
|
257
|
+
ResourceIndicatorValidator.storage_ready?
|
|
258
|
+
end
|
|
259
|
+
|
|
210
260
|
def response_on_fragment?
|
|
211
261
|
return response_type == "token" if response_mode.nil?
|
|
212
262
|
|
|
@@ -65,20 +65,34 @@ module Doorkeeper
|
|
|
65
65
|
Doorkeeper.config.access_token_model.refresh_token_revoked_on_use?
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# RFC 6749 §6: a `scope` parameter that is omitted "is treated as equal
|
|
69
|
+
# to the scope originally granted by the resource owner", and a
|
|
70
|
+
# requested scope must not exceed it. Both are the scope the presented
|
|
71
|
+
# refresh token carries, not the scope of the access token issued with
|
|
72
|
+
# it, which the client may have narrowed on an earlier refresh.
|
|
68
73
|
def default_scopes
|
|
69
|
-
|
|
74
|
+
granted_scopes
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Scope of the presented refresh token. Without the
|
|
78
|
+
# `refresh_token_scopes` column the model reports the access token
|
|
79
|
+
# scope here, which is the behavior Doorkeeper had before the column
|
|
80
|
+
# existed. An access token model that does not implement
|
|
81
|
+
# `refresh_token_scopes` at all (the Sequel and MongoDB adapters ship
|
|
82
|
+
# their own mixins) gets that same behavior.
|
|
83
|
+
def granted_scopes
|
|
84
|
+
@granted_scopes ||= refresh_token.try(:refresh_token_scopes) || refresh_token.scopes
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# True when the access token model implements the granted-scope API
|
|
88
|
+
# and has the column to store it.
|
|
89
|
+
def refresh_token_scopes_supported?
|
|
90
|
+
Doorkeeper.config.access_token_model.try(:refresh_token_scopes_supported?)
|
|
70
91
|
end
|
|
71
92
|
|
|
72
93
|
def create_access_token
|
|
73
94
|
attributes = {}.merge(custom_token_attributes_with_data)
|
|
74
95
|
|
|
75
|
-
resource_owner =
|
|
76
|
-
if Doorkeeper.config.polymorphic_resource_owner?
|
|
77
|
-
refresh_token.resource_owner
|
|
78
|
-
else
|
|
79
|
-
refresh_token.resource_owner_id
|
|
80
|
-
end
|
|
81
|
-
|
|
82
96
|
if refresh_token_revoked_on_use?
|
|
83
97
|
attributes[:previous_refresh_token] = refresh_token.refresh_token
|
|
84
98
|
end
|
|
@@ -96,29 +110,69 @@ module Doorkeeper
|
|
|
96
110
|
attributes[:resource] = refresh_token.resource
|
|
97
111
|
end
|
|
98
112
|
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# with identical or narrower scope (access tokens may have a shorter
|
|
106
|
-
# lifetime and fewer permissions than authorized by the resource
|
|
107
|
-
# owner).
|
|
108
|
-
#
|
|
109
|
-
# Here we assume that TTL of the token received after refreshing should be
|
|
110
|
-
# the same as that of the original token.
|
|
111
|
-
#
|
|
113
|
+
# RFC 6749 §6: "If a new refresh token is issued, the refresh token
|
|
114
|
+
# scope MUST be identical to that of the refresh token included by
|
|
115
|
+
# the client in the request." Carried explicitly so that a narrowed
|
|
116
|
+
# access token does not narrow the refresh token issued with it.
|
|
117
|
+
attributes[:refresh_token_scopes] = granted_scopes.to_s if refresh_token_scopes_supported?
|
|
118
|
+
|
|
112
119
|
@access_token = Doorkeeper.config.access_token_model.create_for(
|
|
113
120
|
application: refresh_token.application,
|
|
114
121
|
resource_owner: resource_owner,
|
|
115
122
|
scopes: scopes,
|
|
116
|
-
expires_in:
|
|
123
|
+
expires_in: access_token_expires_in,
|
|
117
124
|
use_refresh_token: true,
|
|
118
125
|
**attributes,
|
|
119
126
|
)
|
|
120
127
|
end
|
|
121
128
|
|
|
129
|
+
def resource_owner
|
|
130
|
+
if Doorkeeper.config.polymorphic_resource_owner?
|
|
131
|
+
refresh_token.resource_owner
|
|
132
|
+
else
|
|
133
|
+
refresh_token.resource_owner_id
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# RFC6749
|
|
138
|
+
# 1.5. Refresh Token
|
|
139
|
+
#
|
|
140
|
+
# Refresh tokens are issued to the client by the authorization server and are
|
|
141
|
+
# used to obtain a new access token when the current access token
|
|
142
|
+
# becomes invalid or expires, or to obtain additional access tokens
|
|
143
|
+
# with identical or narrower scope (access tokens may have a shorter
|
|
144
|
+
# lifetime and fewer permissions than authorized by the resource
|
|
145
|
+
# owner).
|
|
146
|
+
#
|
|
147
|
+
# The TTL of the refreshed token is therefore that of the original token,
|
|
148
|
+
# so that a lifetime given to the grant the token was first issued with
|
|
149
|
+
# survives refreshing (#1364). +custom_access_token_expires_in+ is still
|
|
150
|
+
# consulted, with +Doorkeeper::OAuth::REFRESH_TOKEN+ as the grant type,
|
|
151
|
+
# so a host can decide the TTL of refreshed tokens explicitly; the TTL
|
|
152
|
+
# of the original token is only inherited when the callable returns nil
|
|
153
|
+
# for this grant (or none is configured).
|
|
154
|
+
#
|
|
155
|
+
# The context carries the resource owner record only with a polymorphic
|
|
156
|
+
# resource owner; otherwise the refresh token stores its id alone, and
|
|
157
|
+
# +resource_owner+ is nil as in the client_credentials grant.
|
|
158
|
+
#
|
|
159
|
+
# The context is built directly rather than through
|
|
160
|
+
# +Authorization::Token.build_context+, which unwraps its argument
|
|
161
|
+
# through #application or #client: that suits the OAuth client the other
|
|
162
|
+
# grants pass, but would replace this application record with whatever
|
|
163
|
+
# an application model responding to either method returns (a
|
|
164
|
+
# +belongs_to :client+ association, say).
|
|
165
|
+
def access_token_expires_in
|
|
166
|
+
context = Authorization::Context.new(
|
|
167
|
+
client: refresh_token.application,
|
|
168
|
+
grant_type: grant_type,
|
|
169
|
+
scopes: scopes,
|
|
170
|
+
resource_owner: Doorkeeper.config.polymorphic_resource_owner? ? resource_owner : nil,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
Authorization::Token.access_token_expires_in(server, context) { refresh_token.expires_in }
|
|
174
|
+
end
|
|
175
|
+
|
|
122
176
|
def validate_token_presence
|
|
123
177
|
@missing_param = :refresh_token if refresh_token.blank? && @refresh_token_parameter.blank?
|
|
124
178
|
|
|
@@ -147,7 +201,7 @@ module Doorkeeper
|
|
|
147
201
|
if @original_scopes.present?
|
|
148
202
|
ScopeChecker.valid?(
|
|
149
203
|
scope_str: @original_scopes,
|
|
150
|
-
server_scopes:
|
|
204
|
+
server_scopes: granted_scopes,
|
|
151
205
|
)
|
|
152
206
|
else
|
|
153
207
|
true
|
|
@@ -171,10 +225,13 @@ module Doorkeeper
|
|
|
171
225
|
# enforce against. Ignore the `resource` parameter.
|
|
172
226
|
return true if validator.nil? && original_resources.blank?
|
|
173
227
|
|
|
228
|
+
# The validator receives the OAuth client, as it does at the
|
|
229
|
+
# authorization endpoint and for every other grant, rather than the
|
|
230
|
+
# application record #client holds here.
|
|
174
231
|
@resolved_resource_indicators = ResourceIndicatorValidator.validate!(
|
|
175
232
|
@raw_resource_indicators,
|
|
176
233
|
config_validator: validator,
|
|
177
|
-
client: client,
|
|
234
|
+
client: client && Doorkeeper::OAuth::Client.new(client),
|
|
178
235
|
grant_resource_indicators: original_resources,
|
|
179
236
|
)
|
|
180
237
|
true
|
|
@@ -13,6 +13,16 @@ module Doorkeeper
|
|
|
13
13
|
module ResourceIndicatorValidator
|
|
14
14
|
module_function
|
|
15
15
|
|
|
16
|
+
# Whether there is anywhere to record the audience a token was restricted
|
|
17
|
+
# to: the `resource` column the doorkeeper:resource_indicators generator
|
|
18
|
+
# adds to both tables. RFC 8707 also needs a policy deciding which
|
|
19
|
+
# resources are acceptable (resource_indicator_validator), so callers
|
|
20
|
+
# asking whether the extension is usable check both.
|
|
21
|
+
def storage_ready?
|
|
22
|
+
Doorkeeper.config.access_grant_model.resource_indicators_supported? &&
|
|
23
|
+
Doorkeeper.config.access_token_model.resource_indicators_supported?
|
|
24
|
+
end
|
|
25
|
+
|
|
16
26
|
# Validates and normalizes an array of resource indicator values.
|
|
17
27
|
#
|
|
18
28
|
# @param resource_indicators [Array<String>, String, nil] One or more resource URIs
|
|
@@ -3,8 +3,53 @@
|
|
|
3
3
|
module Doorkeeper
|
|
4
4
|
module OAuth
|
|
5
5
|
class Token
|
|
6
|
+
# Built-in extractors that read a request parameter, and the parameter
|
|
7
|
+
# each of them reads. RFC 6750 treats the form-encoded body (§2.2) and
|
|
8
|
+
# the URI query string (§2.3) as two distinct transmission methods, but
|
|
9
|
+
# Rack and ActionDispatch both collapse them into a single parameter
|
|
10
|
+
# hash — ActionDispatch lets the query win, Rack lets the body win — so
|
|
11
|
+
# a request carrying the parameter in both would present a single value
|
|
12
|
+
# to the extractor and never be refused. The multi-method check reads
|
|
13
|
+
# the two sources separately for these extractors; selection keeps
|
|
14
|
+
# using the extractor, so which one wins is unchanged.
|
|
15
|
+
PARAMETER_EXTRACTORS = {
|
|
16
|
+
from_access_token_param: "access_token",
|
|
17
|
+
from_bearer_param: "bearer_token",
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
6
20
|
class << self
|
|
21
|
+
# RFC 6750 §2: "Clients MUST NOT use more than one method to transmit
|
|
22
|
+
# the token in each request", and §3.1 lists using more than one method
|
|
23
|
+
# among the conditions an invalid_request answers. Returning the first
|
|
24
|
+
# method that yields a value would discard every other token presented
|
|
25
|
+
# in the same request with no error, warning or log entry, leaving
|
|
26
|
+
# which token authorizes the request to be decided by the configured
|
|
27
|
+
# order of +access_token_methods+ rather than by what the caller sent —
|
|
28
|
+
# so a layer in front of Doorkeeper that reads a different one of them
|
|
29
|
+
# can reach a different verdict about the very same request.
|
|
30
|
+
#
|
|
31
|
+
# What §2 forbids is using more than one method, so the check counts
|
|
32
|
+
# transmission methods rather than comparing the tokens they carry:
|
|
33
|
+
# the same value presented twice is still two methods, and is refused
|
|
34
|
+
# by raising Errors::MultipleAccessTokenMethods so callers can answer
|
|
35
|
+
# with the invalid_request (400) response §3.1 prescribes — the same
|
|
36
|
+
# shape Request.client_authentication_method gives the client
|
|
37
|
+
# authentication side with Errors::MultipleClientAuthMethods.
|
|
38
|
+
#
|
|
39
|
+
# Only the built-in extractors — symbols naming methods on this class,
|
|
40
|
+
# all of them side-effect-free reads of the request — take part in
|
|
41
|
+
# that check. A custom callable extractor is a configuration adapter
|
|
42
|
+
# rather than a transmission method: it keeps the historical
|
|
43
|
+
# first-wins selection and is never invoked more than once, the same
|
|
44
|
+
# exemption client authentication gives its legacy callable
|
|
45
|
+
# extractors (Request#validate_client_authentication!).
|
|
7
46
|
def from_request(request, *methods)
|
|
47
|
+
used = methods.sum do |method|
|
|
48
|
+
method.is_a?(Symbol) ? transmission_methods_used(request, method) : 0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
raise Errors::MultipleAccessTokenMethods if used > 1
|
|
52
|
+
|
|
8
53
|
methods.inject(nil) do |_, method|
|
|
9
54
|
method = self.method(method) if method.is_a?(Symbol)
|
|
10
55
|
credentials = method.call(request)
|
|
@@ -23,11 +68,11 @@ module Doorkeeper
|
|
|
23
68
|
end
|
|
24
69
|
|
|
25
70
|
def from_access_token_param(request)
|
|
26
|
-
request
|
|
71
|
+
parameters(request)[:access_token]
|
|
27
72
|
end
|
|
28
73
|
|
|
29
74
|
def from_bearer_param(request)
|
|
30
|
-
request
|
|
75
|
+
parameters(request)[:bearer_token]
|
|
31
76
|
end
|
|
32
77
|
|
|
33
78
|
def from_bearer_authorization(request)
|
|
@@ -44,6 +89,53 @@ module Doorkeeper
|
|
|
44
89
|
|
|
45
90
|
private
|
|
46
91
|
|
|
92
|
+
# How many transmission methods the given built-in extractor finds a
|
|
93
|
+
# token in. Usually one, or none — but for the parameter extractors
|
|
94
|
+
# above it is the body and the query counted separately, since the
|
|
95
|
+
# parameter hash collapsed them into the single value the extractor
|
|
96
|
+
# reads. That value is counted on its own only when neither raw source
|
|
97
|
+
# explains it, so a token reaching the extractor by some other route —
|
|
98
|
+
# an :access_token path segment, or a host that overrode the extractor
|
|
99
|
+
# — is still counted exactly once rather than twice.
|
|
100
|
+
def transmission_methods_used(request, method)
|
|
101
|
+
value = self.method(method).call(request).presence
|
|
102
|
+
|
|
103
|
+
parameter = PARAMETER_EXTRACTORS[method]
|
|
104
|
+
return value ? 1 : 0 unless parameter
|
|
105
|
+
|
|
106
|
+
sources = parameter_sources(request, parameter)
|
|
107
|
+
sources.size + (value && !sources.include?(value) ? 1 : 0)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The form-encoded body (§2.2) and the URI query (§2.3) as Rack
|
|
111
|
+
# exposes them, for a request object that keeps the two apart.
|
|
112
|
+
def parameter_sources(request, parameter)
|
|
113
|
+
return [] unless request.respond_to?(:GET) && request.respond_to?(:POST)
|
|
114
|
+
|
|
115
|
+
query = request.GET
|
|
116
|
+
body = body_parameters(request)
|
|
117
|
+
return [] unless query.is_a?(Hash) && body.is_a?(Hash)
|
|
118
|
+
|
|
119
|
+
[query[parameter], body[parameter]].filter_map(&:presence)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A body ActionDispatch cannot parse — malformed JSON under a JSON
|
|
123
|
+
# content type, say — carries no token. Reading it must not raise out
|
|
124
|
+
# of the extractors, which host apps call from places that run after
|
|
125
|
+
# their own ParseError handling (instrumentation, exception apps), the
|
|
126
|
+
# same way ActionDispatch's own #filtered_parameters treats that error.
|
|
127
|
+
def parameters(request)
|
|
128
|
+
request.parameters
|
|
129
|
+
rescue ActionDispatch::Http::Parameters::ParseError
|
|
130
|
+
{}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def body_parameters(request)
|
|
134
|
+
request.POST
|
|
135
|
+
rescue ActionDispatch::Http::Parameters::ParseError
|
|
136
|
+
{}
|
|
137
|
+
end
|
|
138
|
+
|
|
47
139
|
def token_from_basic_header(header, pattern)
|
|
48
140
|
encoded_header = token_from_header(header, pattern)
|
|
49
141
|
decode_basic_credentials_token(encoded_header)
|
|
@@ -108,7 +108,7 @@ module Doorkeeper
|
|
|
108
108
|
def success_response
|
|
109
109
|
response = {
|
|
110
110
|
active: true,
|
|
111
|
-
scope:
|
|
111
|
+
scope: scopes_string,
|
|
112
112
|
client_id: @token.try(:application).try(:uid),
|
|
113
113
|
iat: @token.created_at.to_i,
|
|
114
114
|
}
|
|
@@ -207,6 +207,19 @@ module Doorkeeper
|
|
|
207
207
|
@token_type == :refresh_token
|
|
208
208
|
end
|
|
209
209
|
|
|
210
|
+
# RFC 7662 §2.2: `scope` describes the presented token. A refresh token
|
|
211
|
+
# carries the scope originally granted by the resource owner
|
|
212
|
+
# (RFC 6749 §6), which can be wider than the scope of the access token
|
|
213
|
+
# it was issued with when the client narrowed that one on a refresh.
|
|
214
|
+
# Models without a stored granted scope (no `refresh_token_scopes`
|
|
215
|
+
# column, or a row created before its migration) report the access
|
|
216
|
+
# token scope, as before.
|
|
217
|
+
def scopes_string
|
|
218
|
+
return @token.scopes_string unless refresh_token_presented?
|
|
219
|
+
|
|
220
|
+
@token.try(:refresh_token_scopes_string).presence || @token.scopes_string
|
|
221
|
+
end
|
|
222
|
+
|
|
210
223
|
def valid_authorized_token?
|
|
211
224
|
!authorized_token_matches_introspected? &&
|
|
212
225
|
authorized_token.accessible? &&
|
|
@@ -9,6 +9,7 @@ module Doorkeeper::Orm::ActiveRecord::Mixins
|
|
|
9
9
|
self.strict_loading_by_default = false if respond_to?(:strict_loading_by_default)
|
|
10
10
|
|
|
11
11
|
include ::Doorkeeper::AccessGrantMixin
|
|
12
|
+
include ::Doorkeeper::Orm::ActiveRecord::Mixins::SecretStorable
|
|
12
13
|
include ::Doorkeeper::Models::PolymorphicResourceOwner::ForAccessGrant
|
|
13
14
|
|
|
14
15
|
belongs_to :application, class_name: Doorkeeper.config.application_class.to_s,
|
|
@@ -9,6 +9,7 @@ module Doorkeeper::Orm::ActiveRecord::Mixins
|
|
|
9
9
|
self.strict_loading_by_default = false if respond_to?(:strict_loading_by_default)
|
|
10
10
|
|
|
11
11
|
include ::Doorkeeper::AccessTokenMixin
|
|
12
|
+
include ::Doorkeeper::Orm::ActiveRecord::Mixins::SecretStorable
|
|
12
13
|
include ::Doorkeeper::Models::PolymorphicResourceOwner::ForAccessToken
|
|
13
14
|
|
|
14
15
|
belongs_to :application, class_name: Doorkeeper.config.application_class.to_s,
|
|
@@ -9,6 +9,7 @@ module Doorkeeper::Orm::ActiveRecord::Mixins
|
|
|
9
9
|
self.strict_loading_by_default = false if respond_to?(:strict_loading_by_default)
|
|
10
10
|
|
|
11
11
|
include ::Doorkeeper::ApplicationMixin
|
|
12
|
+
include ::Doorkeeper::Orm::ActiveRecord::Mixins::SecretStorable
|
|
12
13
|
# `enable_application_owner?` is read once, at parent-class autoload
|
|
13
14
|
# time (#1831): with the feature off the model exposes no `:owner`
|
|
14
15
|
# association — avoiding a misleading reflection on schemas that lack
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper::Orm::ActiveRecord::Mixins
|
|
4
|
+
# Active Record implementation of the SecretStorable write hook: the
|
|
5
|
+
# fallback secret upgrade only writes while the column still holds the
|
|
6
|
+
# value the lookup matched, so losing a race against a concurrent write —
|
|
7
|
+
# another request renewing the secret, or the application replacing it —
|
|
8
|
+
# cannot put the superseded secret back.
|
|
9
|
+
#
|
|
10
|
+
# The write is a single conditional statement, so unlike the historical
|
|
11
|
+
# `#update` it runs no model callbacks: what it writes is the value the
|
|
12
|
+
# lookup already matched, re-encoded into the current storage format.
|
|
13
|
+
module SecretStorable
|
|
14
|
+
extend ActiveSupport::Concern
|
|
15
|
+
|
|
16
|
+
class_methods do
|
|
17
|
+
# Writes the upgraded secret over +matched+ on +instance+'s row,
|
|
18
|
+
# conditional on +attr+ still holding +matched+, and answers whether
|
|
19
|
+
# the row was written.
|
|
20
|
+
def write_upgraded_secret(instance, attr, matched, _upgraded)
|
|
21
|
+
# The value written is the one `store_secret` left on the instance:
|
|
22
|
+
# it assigns through the attribute writer, which `#update` then ran
|
|
23
|
+
# again on assignment, so a custom writer override reached storage.
|
|
24
|
+
# `update_all` does not go through writers, so writing the strategy's
|
|
25
|
+
# pre-writer return value would both skip the override and leave the
|
|
26
|
+
# instance out of step with its row.
|
|
27
|
+
upgraded = instance.read_attribute(attr)
|
|
28
|
+
|
|
29
|
+
# `update_all` does not maintain timestamps, which `#update` did —
|
|
30
|
+
# see `upgrade_timestamps` for the terms it did that on.
|
|
31
|
+
changes = { attr => upgraded }.merge!(upgrade_timestamps(instance))
|
|
32
|
+
|
|
33
|
+
scope = where(primary_key_conditions(instance).merge(attr => matched))
|
|
34
|
+
|
|
35
|
+
# Under optimistic locking, `update_all` bumps the lock column on
|
|
36
|
+
# its own, which would leave the instance stale and have its next
|
|
37
|
+
# `save` refused. Write the bump explicitly instead — `update_all`
|
|
38
|
+
# leaves the column alone when it is among the changes — so that the
|
|
39
|
+
# instance can be brought in step below, and make the write
|
|
40
|
+
# conditional on the version too: bumping from a version the row no
|
|
41
|
+
# longer holds would set it backwards.
|
|
42
|
+
if locking_enabled?
|
|
43
|
+
version = instance.public_send(locking_column) || 0
|
|
44
|
+
scope = scope.where(locking_column => version)
|
|
45
|
+
changes[locking_column] = version + 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The write must reach the primary database when automatic role
|
|
49
|
+
# switching would route the surrounding request to a read replica.
|
|
50
|
+
written = with_primary_role { scope.update_all(changes) }
|
|
51
|
+
return false if written.zero?
|
|
52
|
+
|
|
53
|
+
sync_upgraded_instance(instance, changes)
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Restores +matched+ on the attribute directly, past any custom
|
|
58
|
+
# writer: re-running a writer that transforms its input would leave
|
|
59
|
+
# the attribute dirty with a value the row never held, and a later
|
|
60
|
+
# save would write that over whatever replaced the matched secret.
|
|
61
|
+
# The change is then cleared — the attribute holds what it held when
|
|
62
|
+
# the row was read, so there is nothing left to save.
|
|
63
|
+
def restore_matched_secret(instance, attr, matched)
|
|
64
|
+
instance.write_attribute(attr, matched)
|
|
65
|
+
instance.clear_attribute_changes([attr])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# The columns identifying +instance+'s row and the values it holds in
|
|
71
|
+
# them, taken from the configured primary key: it may be named
|
|
72
|
+
# something other than `id`, or be composed of several columns. A
|
|
73
|
+
# model with no primary key at all is left to the value condition
|
|
74
|
+
# alone, which reaches every row still holding the matched secret.
|
|
75
|
+
def primary_key_conditions(instance)
|
|
76
|
+
# Spelled without `index_with`, which the gemspec's Rails floor
|
|
77
|
+
# predates.
|
|
78
|
+
Array(primary_key).to_h { |key| [key, instance.public_send(key)] } # rubocop:disable Rails/IndexWith
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The timestamps `#update` maintained for this write, derived on its
|
|
82
|
+
# terms through Active Record's own helpers: the model's actual update
|
|
83
|
+
# timestamp columns (`updated_on` and aliased names as well as
|
|
84
|
+
# `updated_at`), the connection's timezone, and `record_timestamps`
|
|
85
|
+
# read on the instance, where a model that stamps its own timestamps
|
|
86
|
+
# can turn it off per record. A timestamp the caller had already
|
|
87
|
+
# changed is left out entirely, as `#update` leaves it to the caller —
|
|
88
|
+
# stamping it and then marking it clean below would silently drop that
|
|
89
|
+
# pending change.
|
|
90
|
+
def upgrade_timestamps(instance)
|
|
91
|
+
return {} unless instance.record_timestamps
|
|
92
|
+
|
|
93
|
+
touch_attributes_with_time.reject do |column, _time|
|
|
94
|
+
instance.will_save_change_to_attribute?(column)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The row was written past the instance, so bring it back in step:
|
|
99
|
+
# left as it is, a caller that later saves or locks the record is
|
|
100
|
+
# refused over a change that is already persisted. Only what was
|
|
101
|
+
# written is marked clean — a `reload` would also discard every
|
|
102
|
+
# unrelated unsaved change the caller had on the instance, and this is
|
|
103
|
+
# nominally a read path.
|
|
104
|
+
def sync_upgraded_instance(instance, changes)
|
|
105
|
+
timestamp_attributes_for_update_in_model.each do |column|
|
|
106
|
+
instance.public_send(:"#{column}=", changes[column]) if changes.key?(column)
|
|
107
|
+
end
|
|
108
|
+
instance.public_send(:"#{locking_column}=", changes[locking_column]) if locking_enabled?
|
|
109
|
+
instance.clear_attribute_changes(changes.keys)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -34,8 +34,12 @@ module Doorkeeper
|
|
|
34
34
|
Doorkeeper::OAuth::NonStandard::IETF_WG_OAUTH2_OOB_METHODS.include?(uri)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Script schemes are refused before the host application's rule is
|
|
38
|
+
# consulted, so a `forbid_redirect_uri` that rejects them too does not
|
|
39
|
+
# report the error twice.
|
|
37
40
|
def forbidden_uri?(uri)
|
|
38
|
-
Doorkeeper.
|
|
41
|
+
Doorkeeper::OAuth::Helpers::URIChecker.script_scheme?(uri) ||
|
|
42
|
+
Doorkeeper.config.forbid_redirect_uri.call(uri)
|
|
39
43
|
end
|
|
40
44
|
|
|
41
45
|
def unspecified_scheme?(uri)
|
|
@@ -26,6 +26,7 @@ module Doorkeeper
|
|
|
26
26
|
autoload :AccessGrant, "doorkeeper/orm/active_record/mixins/access_grant"
|
|
27
27
|
autoload :AccessToken, "doorkeeper/orm/active_record/mixins/access_token"
|
|
28
28
|
autoload :Application, "doorkeeper/orm/active_record/mixins/application"
|
|
29
|
+
autoload :SecretStorable, "doorkeeper/orm/active_record/mixins/secret_storable"
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
# Kept as a no-op so `Doorkeeper.run_orm_hooks` (and any plugin that
|