doorkeeper 6.0.0.beta1 → 6.0.0.beta2
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 +35 -1
- data/README.md +203 -0
- data/app/controllers/doorkeeper/applications_controller.rb +38 -9
- data/app/controllers/doorkeeper/authorizations_controller.rb +17 -3
- data/app/controllers/doorkeeper/tokens_controller.rb +5 -1
- data/app/views/doorkeeper/authorizations/new.html.erb +18 -0
- data/config/locales/en.yml +2 -0
- data/lib/doorkeeper/client_authentication/legacy_callable.rb +6 -0
- data/lib/doorkeeper/client_authentication/method.rb +17 -0
- data/lib/doorkeeper/client_authentication/verified_credentials.rb +19 -0
- data/lib/doorkeeper/client_authentication.rb +9 -0
- data/lib/doorkeeper/config.rb +47 -0
- data/lib/doorkeeper/document_cache.rb +81 -0
- data/lib/doorkeeper/errors.rb +48 -0
- data/lib/doorkeeper/http_fetcher.rb +232 -0
- data/lib/doorkeeper/models/access_grant_mixin.rb +16 -0
- data/lib/doorkeeper/models/access_token_mixin.rb +42 -1
- data/lib/doorkeeper/oauth/authorization/code.rb +10 -0
- data/lib/doorkeeper/oauth/authorization/token.rb +13 -2
- data/lib/doorkeeper/oauth/authorization_code_request.rb +123 -2
- data/lib/doorkeeper/oauth/base_request.rb +1 -2
- data/lib/doorkeeper/oauth/client.rb +6 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +4 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_post.rb +4 -0
- data/lib/doorkeeper/oauth/client_authentication/none.rb +47 -8
- data/lib/doorkeeper/oauth/client_authentication/private_key_jwt/key_resolver.rb +104 -0
- data/lib/doorkeeper/oauth/client_authentication/private_key_jwt/replay_guard.rb +78 -0
- data/lib/doorkeeper/oauth/client_authentication/private_key_jwt.rb +247 -0
- data/lib/doorkeeper/oauth/client_credentials/creator.rb +25 -9
- data/lib/doorkeeper/oauth/client_credentials_request.rb +36 -5
- data/lib/doorkeeper/oauth/helpers/uri_checker.rb +35 -23
- data/lib/doorkeeper/oauth/metadata_response.rb +8 -0
- data/lib/doorkeeper/oauth/password_access_token_request.rb +27 -1
- data/lib/doorkeeper/oauth/pre_authorization.rb +51 -3
- data/lib/doorkeeper/oauth/refresh_token_request.rb +51 -1
- data/lib/doorkeeper/oauth/resource_indicator_validator.rb +70 -0
- data/lib/doorkeeper/oauth/scopes.rb +25 -0
- data/lib/doorkeeper/oauth/token_introspection.rb +6 -0
- data/lib/doorkeeper/version.rb +1 -1
- data/lib/doorkeeper.rb +4 -0
- data/lib/generators/doorkeeper/grant_reuse_revocation_generator.rb +43 -0
- data/lib/generators/doorkeeper/resource_indicators_generator.rb +33 -0
- data/lib/generators/doorkeeper/templates/add_access_token_to_access_grants.rb.erb +10 -0
- data/lib/generators/doorkeeper/templates/enable_resource_indicators_migration.rb.erb +8 -0
- data/lib/generators/doorkeeper/templates/initializer.rb +65 -0
- data/lib/generators/doorkeeper/templates/migration.rb.erb +13 -0
- metadata +45 -3
|
@@ -15,27 +15,42 @@ module Doorkeeper
|
|
|
15
15
|
false
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# RFC6749, Section 3.1.2.3 requires the requested redirect URI to be
|
|
19
|
+
# compared to the registered redirect URIs using the simple string
|
|
20
|
+
# comparison defined in RFC3986, Section 6.2.1.
|
|
21
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2.3
|
|
18
22
|
def self.matches?(url, client_url)
|
|
23
|
+
return true if url == client_url
|
|
24
|
+
|
|
25
|
+
# RFC8252, Paragraph 7.3 allows the port of loopback interface
|
|
26
|
+
# redirect URIs to vary at runtime, so it is ignored when both
|
|
27
|
+
# URIs point to the loopback interface.
|
|
28
|
+
# @see https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
|
|
19
29
|
url = as_uri(url)
|
|
20
30
|
client_url = as_uri(client_url)
|
|
21
31
|
|
|
22
|
-
unless client_url
|
|
23
|
-
return false unless query_matches?(url.query, client_url.query)
|
|
24
|
-
|
|
25
|
-
# Clear out queries so rest of URI can be tested. This allows query
|
|
26
|
-
# params to be in the request but order not mattering.
|
|
27
|
-
client_url.query = nil
|
|
28
|
-
end
|
|
32
|
+
return false unless loopback_uri?(url) && loopback_uri?(client_url)
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
client_url.port = nil
|
|
35
|
-
end
|
|
34
|
+
urls_match?(url, client_url)
|
|
35
|
+
rescue URI::InvalidURIError
|
|
36
|
+
false
|
|
37
|
+
end
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
# RFC 8252 §7.3 lets only the PORT of a loopback redirect URI vary at
|
|
40
|
+
# runtime. Compare every other component explicitly instead of blanking
|
|
41
|
+
# the port and comparing the reassembled strings: `URI#port=` also
|
|
42
|
+
# clears the userinfo on Ruby >= 4.0, which would let
|
|
43
|
+
# `http://attacker@127.0.0.1/cb` match a registered
|
|
44
|
+
# `http://127.0.0.1/cb`. Comparing components (not `URI#==`) also keeps
|
|
45
|
+
# the exception from widening past the port — e.g. an empty path is not
|
|
46
|
+
# treated as equal to "/".
|
|
47
|
+
def self.urls_match?(url, client_url)
|
|
48
|
+
url.scheme == client_url.scheme &&
|
|
49
|
+
url.userinfo == client_url.userinfo &&
|
|
50
|
+
url.host == client_url.host &&
|
|
51
|
+
url.path == client_url.path &&
|
|
52
|
+
url.query == client_url.query &&
|
|
53
|
+
url.fragment == client_url.fragment
|
|
39
54
|
end
|
|
40
55
|
|
|
41
56
|
def self.loopback_uri?(uri)
|
|
@@ -45,6 +60,11 @@ module Doorkeeper
|
|
|
45
60
|
end
|
|
46
61
|
|
|
47
62
|
def self.valid_for_authorization?(url, client_url)
|
|
63
|
+
# client_url is blank when the application was registered without a
|
|
64
|
+
# redirect URI (allow_blank_redirect_uri) — no possible match for
|
|
65
|
+
# redirection based OAuth flows, not an error.
|
|
66
|
+
return false if client_url.blank?
|
|
67
|
+
|
|
48
68
|
valid?(url) && client_url.split.any? { |other_url| matches?(url, other_url) }
|
|
49
69
|
end
|
|
50
70
|
|
|
@@ -52,14 +72,6 @@ module Doorkeeper
|
|
|
52
72
|
URI.parse(url)
|
|
53
73
|
end
|
|
54
74
|
|
|
55
|
-
def self.query_matches?(query, client_query)
|
|
56
|
-
return true if client_query.blank? && query.blank?
|
|
57
|
-
return false if client_query.nil? || query.nil?
|
|
58
|
-
|
|
59
|
-
# Will return true independent of query order
|
|
60
|
-
client_query.split("&").sort == query.split("&").sort
|
|
61
|
-
end
|
|
62
|
-
|
|
63
75
|
def self.valid_scheme?(uri)
|
|
64
76
|
return false if uri.scheme.blank?
|
|
65
77
|
|
|
@@ -35,6 +35,8 @@ module Doorkeeper
|
|
|
35
35
|
# condition under which the iss parameter is emitted. false survives
|
|
36
36
|
# the compaction below, so it is advertised explicitly.
|
|
37
37
|
authorization_response_iss_parameter_supported: config.issuer.present?,
|
|
38
|
+
# RFC 8707: advertise resource indicator support when configured.
|
|
39
|
+
resource_indicators_supported: resource_indicators_supported?,
|
|
38
40
|
}
|
|
39
41
|
data.compact!
|
|
40
42
|
|
|
@@ -152,6 +154,12 @@ module Doorkeeper
|
|
|
152
154
|
def code_challenge_methods_supported
|
|
153
155
|
config.pkce_code_challenge_methods_supported
|
|
154
156
|
end
|
|
157
|
+
|
|
158
|
+
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
|
+
end
|
|
155
163
|
end
|
|
156
164
|
end
|
|
157
165
|
end
|
|
@@ -9,10 +9,12 @@ module Doorkeeper
|
|
|
9
9
|
validate :client_supports_grant_flow, error: Errors::UnauthorizedClient
|
|
10
10
|
validate :resource_owner, error: Errors::InvalidGrant
|
|
11
11
|
validate :scopes, error: Errors::InvalidScope
|
|
12
|
+
validate :resource_indicators, error: Errors::InvalidTarget
|
|
12
13
|
|
|
13
14
|
attr_reader :client, :credentials, :resource_owner, :parameters, :access_token
|
|
14
15
|
|
|
15
16
|
def initialize(server, client, credentials, resource_owner, parameters = {})
|
|
17
|
+
super()
|
|
16
18
|
@server = server
|
|
17
19
|
@resource_owner = resource_owner
|
|
18
20
|
@client = client
|
|
@@ -20,12 +22,21 @@ module Doorkeeper
|
|
|
20
22
|
@parameters = parameters
|
|
21
23
|
@original_scopes = parameters[:scope]
|
|
22
24
|
@grant_type = Doorkeeper::OAuth::PASSWORD
|
|
25
|
+
@raw_resource_indicators = parameters[:resource]
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
private
|
|
26
29
|
|
|
27
30
|
def before_successful_response
|
|
28
|
-
|
|
31
|
+
token_attributes = {}
|
|
32
|
+
if @resolved_resource_indicators.present?
|
|
33
|
+
unless Doorkeeper.config.access_token_model.resource_indicators_supported?
|
|
34
|
+
raise Errors::MissingResourceColumn, "oauth_access_tokens"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
token_attributes[:resource] = @resolved_resource_indicators.join(" ")
|
|
38
|
+
end
|
|
39
|
+
find_or_create_access_token(client, resource_owner, scopes, token_attributes, server)
|
|
29
40
|
super
|
|
30
41
|
end
|
|
31
42
|
|
|
@@ -70,6 +81,21 @@ module Doorkeeper
|
|
|
70
81
|
def validate_client_supports_grant_flow
|
|
71
82
|
Doorkeeper.config.allow_grant_flow_for_client?(grant_type, client&.application)
|
|
72
83
|
end
|
|
84
|
+
|
|
85
|
+
# RFC 8707: validate resource indicators against server policy.
|
|
86
|
+
def validate_resource_indicators
|
|
87
|
+
validator = Doorkeeper.config.resource_indicator_validator
|
|
88
|
+
return true unless validator
|
|
89
|
+
|
|
90
|
+
@resolved_resource_indicators = ResourceIndicatorValidator.validate!(
|
|
91
|
+
@raw_resource_indicators,
|
|
92
|
+
config_validator: validator,
|
|
93
|
+
client: client,
|
|
94
|
+
)
|
|
95
|
+
true
|
|
96
|
+
rescue Errors::InvalidTarget
|
|
97
|
+
false
|
|
98
|
+
end
|
|
73
99
|
end
|
|
74
100
|
end
|
|
75
101
|
end
|
|
@@ -16,11 +16,12 @@ module Doorkeeper
|
|
|
16
16
|
validate :scopes, error: Errors::InvalidScope
|
|
17
17
|
validate :code_challenge, error: Errors::InvalidRequest
|
|
18
18
|
validate :code_challenge_method, error: Errors::InvalidCodeChallengeMethod
|
|
19
|
+
validate :resource_indicators, error: Errors::InvalidTarget
|
|
19
20
|
|
|
20
21
|
attr_reader :client, :code_challenge, :code_challenge_method, :missing_param,
|
|
21
22
|
:redirect_uri, :resource_owner, :response_type, :state,
|
|
22
23
|
:authorization_response_flow, :response_mode, :custom_access_token_attributes,
|
|
23
|
-
:invalid_request_reason
|
|
24
|
+
:invalid_request_reason, :resource_indicators
|
|
24
25
|
|
|
25
26
|
def initialize(server, parameters = {}, resource_owner = nil)
|
|
26
27
|
@server = server
|
|
@@ -34,6 +35,7 @@ module Doorkeeper
|
|
|
34
35
|
@code_challenge_method = parameters[:code_challenge_method]
|
|
35
36
|
@resource_owner = resource_owner
|
|
36
37
|
@custom_access_token_attributes = parameters.slice(*Doorkeeper.config.custom_access_token_attributes).to_h
|
|
38
|
+
@raw_resource_indicators = parameters[:resource]
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
def authorizable?
|
|
@@ -84,7 +86,7 @@ module Doorkeeper
|
|
|
84
86
|
if client_scopes.blank?
|
|
85
87
|
server.default_scopes.to_s
|
|
86
88
|
else
|
|
87
|
-
server.default_scopes.
|
|
89
|
+
server.default_scopes.common(client_scopes).to_s
|
|
88
90
|
end
|
|
89
91
|
end
|
|
90
92
|
|
|
@@ -123,7 +125,21 @@ module Doorkeeper
|
|
|
123
125
|
:scope
|
|
124
126
|
end
|
|
125
127
|
|
|
126
|
-
@missing_param.nil?
|
|
128
|
+
return false unless @missing_param.nil?
|
|
129
|
+
|
|
130
|
+
# A structured `scope` (e.g. `scope[a]=b`, parsed by Rack into a Hash)
|
|
131
|
+
# is malformed (RFC 6749 §3.3). Reject it here — before validate_scopes
|
|
132
|
+
# reaches Scopes.from_string — so the authorization endpoint answers
|
|
133
|
+
# invalid_request instead of letting the raised error surface as a 500.
|
|
134
|
+
# Set a reason so the error_description is not translated from nil.
|
|
135
|
+
return true if scope_param_well_formed?
|
|
136
|
+
|
|
137
|
+
@invalid_request_reason = :unknown
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def scope_param_well_formed?
|
|
142
|
+
@scope.nil? || @scope.is_a?(String)
|
|
127
143
|
end
|
|
128
144
|
|
|
129
145
|
def validate_response_type
|
|
@@ -155,6 +171,14 @@ module Doorkeeper
|
|
|
155
171
|
|
|
156
172
|
def validate_code_challenge
|
|
157
173
|
return true unless Doorkeeper.config.force_pkce?
|
|
174
|
+
# PKCE (RFC 7636) protects the exchange of an authorization code, so
|
|
175
|
+
# a code_challenge is only required from response types that issue one
|
|
176
|
+
# ("code" and code-carrying hybrid types like "code id_token"). For
|
|
177
|
+
# response types that never issue a code (e.g. "token" or an OIDC
|
|
178
|
+
# extension's "id_token"), there is no token-endpoint exchange where a
|
|
179
|
+
# verifier could ever be checked, so requiring a challenge would
|
|
180
|
+
# reject those requests over a parameter that cannot be validated.
|
|
181
|
+
return true unless code_issuing_response_type?
|
|
158
182
|
return true if code_challenge.present?
|
|
159
183
|
|
|
160
184
|
@invalid_request_reason = :invalid_code_challenge
|
|
@@ -168,6 +192,21 @@ module Doorkeeper
|
|
|
168
192
|
(code_challenge_method.present? && Doorkeeper.config.pkce_code_challenge_methods_supported.include?(code_challenge_method))
|
|
169
193
|
end
|
|
170
194
|
|
|
195
|
+
def validate_resource_indicators
|
|
196
|
+
validator = Doorkeeper.config.resource_indicator_validator
|
|
197
|
+
# When no validator is configured, resource indicators are ignored (feature disabled)
|
|
198
|
+
return true unless validator
|
|
199
|
+
|
|
200
|
+
@resource_indicators = ResourceIndicatorValidator.validate!(
|
|
201
|
+
@raw_resource_indicators,
|
|
202
|
+
config_validator: validator,
|
|
203
|
+
client: client,
|
|
204
|
+
)
|
|
205
|
+
true
|
|
206
|
+
rescue Errors::InvalidTarget
|
|
207
|
+
false
|
|
208
|
+
end
|
|
209
|
+
|
|
171
210
|
def response_on_fragment?
|
|
172
211
|
return response_type == "token" if response_mode.nil?
|
|
173
212
|
|
|
@@ -178,6 +217,15 @@ module Doorkeeper
|
|
|
178
217
|
response_type == "code" ? AUTHORIZATION_CODE : IMPLICIT
|
|
179
218
|
end
|
|
180
219
|
|
|
220
|
+
# Whether the requested response type issues an authorization code.
|
|
221
|
+
# Multi-valued response types (OIDC hybrid flows registered by
|
|
222
|
+
# extensions, e.g. "code id_token") are space-delimited per OAuth 2.0
|
|
223
|
+
# Multiple Response Type Encoding Practices, so a token-wise check also
|
|
224
|
+
# covers response types Doorkeeper itself does not ship.
|
|
225
|
+
def code_issuing_response_type?
|
|
226
|
+
response_type.to_s.split.include?("code")
|
|
227
|
+
end
|
|
228
|
+
|
|
181
229
|
def pre_auth_hash
|
|
182
230
|
{
|
|
183
231
|
client_id: client.uid,
|
|
@@ -10,6 +10,7 @@ module Doorkeeper
|
|
|
10
10
|
validate :client, error: Errors::InvalidClient
|
|
11
11
|
validate :client_match, error: Errors::InvalidGrant
|
|
12
12
|
validate :scope, error: Errors::InvalidScope
|
|
13
|
+
validate :resource_indicators, error: Errors::InvalidTarget
|
|
13
14
|
|
|
14
15
|
attr_reader :access_token, :client, :credentials, :refresh_token
|
|
15
16
|
attr_reader :missing_param
|
|
@@ -21,13 +22,21 @@ module Doorkeeper
|
|
|
21
22
|
@grant_type = Doorkeeper::OAuth::REFRESH_TOKEN
|
|
22
23
|
@original_scopes = parameters[:scope] || parameters[:scopes]
|
|
23
24
|
@refresh_token_parameter = parameters[:refresh_token]
|
|
25
|
+
@raw_resource_indicators = parameters[:resource]
|
|
24
26
|
@client = load_client(credentials) if credentials
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
private
|
|
28
30
|
|
|
31
|
+
# Resolved through +OAuth::Client+ rather than by looking the row up
|
|
32
|
+
# directly, so this grant applies the same rules the other token
|
|
33
|
+
# endpoint grants do: credentials that carry no secret because their
|
|
34
|
+
# authentication method already proved the client's identity
|
|
35
|
+
# (private_key_jwt) resolve by uid alone. The application record is
|
|
36
|
+
# what is returned, since that is what +#client+ has always exposed
|
|
37
|
+
# here.
|
|
29
38
|
def load_client(credentials)
|
|
30
|
-
Doorkeeper.
|
|
39
|
+
Doorkeeper::OAuth::Client.authenticate(credentials)&.application
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
def before_successful_response
|
|
@@ -74,6 +83,19 @@ module Doorkeeper
|
|
|
74
83
|
attributes[:previous_refresh_token] = refresh_token.refresh_token
|
|
75
84
|
end
|
|
76
85
|
|
|
86
|
+
# RFC 8707: carry resource indicators to the new access token.
|
|
87
|
+
# If the refresh request specified a (subset of) resource(s), use those;
|
|
88
|
+
# otherwise inherit from the refresh token itself.
|
|
89
|
+
if @resolved_resource_indicators.present?
|
|
90
|
+
unless Doorkeeper.config.access_token_model.resource_indicators_supported?
|
|
91
|
+
raise Errors::MissingResourceColumn, "oauth_access_tokens"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
attributes[:resource] = @resolved_resource_indicators.join(" ")
|
|
95
|
+
elsif refresh_token.try(:resource).present?
|
|
96
|
+
attributes[:resource] = refresh_token.resource
|
|
97
|
+
end
|
|
98
|
+
|
|
77
99
|
# RFC6749
|
|
78
100
|
# 1.5. Refresh Token
|
|
79
101
|
#
|
|
@@ -132,6 +154,34 @@ module Doorkeeper
|
|
|
132
154
|
end
|
|
133
155
|
end
|
|
134
156
|
|
|
157
|
+
# RFC 8707: resource indicators on refresh must be a subset of those
|
|
158
|
+
# bound to the original refresh token (which inherited from the grant).
|
|
159
|
+
#
|
|
160
|
+
# Subset and syntax enforcement run even when no validator is configured
|
|
161
|
+
# as long as the original token is already audience-restricted: a refresh
|
|
162
|
+
# must never widen the audience beyond what the original token carried.
|
|
163
|
+
# Only when the feature is disabled AND the original token has no stored
|
|
164
|
+
# resources is the `resource` parameter ignored entirely.
|
|
165
|
+
def validate_resource_indicators
|
|
166
|
+
original_resources = refresh_token.try(:resource)&.split
|
|
167
|
+
|
|
168
|
+
validator = Doorkeeper.config.resource_indicator_validator
|
|
169
|
+
|
|
170
|
+
# Feature effectively off: no validator and nothing already bound to
|
|
171
|
+
# enforce against. Ignore the `resource` parameter.
|
|
172
|
+
return true if validator.nil? && original_resources.blank?
|
|
173
|
+
|
|
174
|
+
@resolved_resource_indicators = ResourceIndicatorValidator.validate!(
|
|
175
|
+
@raw_resource_indicators,
|
|
176
|
+
config_validator: validator,
|
|
177
|
+
client: client,
|
|
178
|
+
grant_resource_indicators: original_resources,
|
|
179
|
+
)
|
|
180
|
+
true
|
|
181
|
+
rescue Errors::InvalidTarget
|
|
182
|
+
false
|
|
183
|
+
end
|
|
184
|
+
|
|
135
185
|
def custom_token_attributes_with_data
|
|
136
186
|
refresh_token
|
|
137
187
|
.attributes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Doorkeeper
|
|
6
|
+
module OAuth
|
|
7
|
+
# Validates the `resource` parameter per RFC 8707.
|
|
8
|
+
#
|
|
9
|
+
# The `resource` value MUST be an absolute URI without a fragment component.
|
|
10
|
+
# Multiple `resource` parameters MAY be present.
|
|
11
|
+
#
|
|
12
|
+
# @see https://datatracker.ietf.org/doc/html/rfc8707#section-2
|
|
13
|
+
module ResourceIndicatorValidator
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Validates and normalizes an array of resource indicator values.
|
|
17
|
+
#
|
|
18
|
+
# @param resource_indicators [Array<String>, String, nil] One or more resource URIs
|
|
19
|
+
# @param config_validator [Proc, nil] Custom validator from configuration
|
|
20
|
+
# @param client [Doorkeeper::OAuth::Client, nil] The requesting client
|
|
21
|
+
# @param grant_resource_indicators [Array<String>, nil] Resource indicators from the
|
|
22
|
+
# original authorization grant (for subset enforcement on token requests)
|
|
23
|
+
#
|
|
24
|
+
# @return [Array<String>] Validated resource indicator URIs
|
|
25
|
+
# @raise [Doorkeeper::Errors::InvalidTarget] When validation fails
|
|
26
|
+
def validate!(resource_indicators, config_validator: nil, client: nil, grant_resource_indicators: nil)
|
|
27
|
+
return [] if resource_indicators.blank?
|
|
28
|
+
|
|
29
|
+
indicators = Array.wrap(resource_indicators).reject(&:blank?).uniq
|
|
30
|
+
return [] if indicators.empty?
|
|
31
|
+
|
|
32
|
+
indicators.each { |uri| validate_uri!(uri) }
|
|
33
|
+
|
|
34
|
+
# If the token request specifies resources, they must be a subset of those
|
|
35
|
+
# originally granted (RFC 8707 §2.2).
|
|
36
|
+
raise Errors::InvalidTarget if grant_resource_indicators.present? && (indicators - grant_resource_indicators).any?
|
|
37
|
+
|
|
38
|
+
# Custom server policy validation
|
|
39
|
+
raise Errors::InvalidTarget if config_validator && !config_validator.call(indicators, client)
|
|
40
|
+
|
|
41
|
+
indicators
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Boolean] true when the values are syntactically valid
|
|
45
|
+
def valid?(resource_indicators)
|
|
46
|
+
return true if resource_indicators.blank?
|
|
47
|
+
|
|
48
|
+
Array(resource_indicators).reject(&:blank?).all? { |uri| valid_uri?(uri) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def valid_uri?(uri)
|
|
52
|
+
return false unless uri.is_a?(String)
|
|
53
|
+
|
|
54
|
+
parsed = URI.parse(uri)
|
|
55
|
+
# MUST be absolute URI
|
|
56
|
+
return false unless parsed.absolute?
|
|
57
|
+
# MUST NOT include a fragment
|
|
58
|
+
return false if parsed.fragment
|
|
59
|
+
|
|
60
|
+
true
|
|
61
|
+
rescue URI::InvalidURIError, TypeError
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_uri!(uri)
|
|
66
|
+
raise Errors::InvalidTarget unless valid_uri?(uri)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -10,6 +10,14 @@ module Doorkeeper
|
|
|
10
10
|
|
|
11
11
|
def self.from_string(string)
|
|
12
12
|
string ||= ""
|
|
13
|
+
|
|
14
|
+
# A non-string scope (e.g. `scope[a]=b`, parsed by Rack into a Hash)
|
|
15
|
+
# has no octets to split into tokens. Reject it as a malformed request
|
|
16
|
+
# (RFC 6749 §3.3); at the token endpoint TokensController's rescue_from
|
|
17
|
+
# turns this into invalid_request. Callers that always pass a string
|
|
18
|
+
# (the model layer) never hit this.
|
|
19
|
+
raise Errors::InvalidScopeParameter, "scope must be a String" unless string.is_a?(String)
|
|
20
|
+
|
|
13
21
|
new.tap do |scope|
|
|
14
22
|
scope.add(*string.split)
|
|
15
23
|
end
|
|
@@ -92,6 +100,23 @@ module Doorkeeper
|
|
|
92
100
|
self.class.from_array(filtered_scopes)
|
|
93
101
|
end
|
|
94
102
|
|
|
103
|
+
# Returns the scopes present in both sets, taking dynamic scopes
|
|
104
|
+
# into account: a dynamic scope pattern (e.g. `user:*`) on either
|
|
105
|
+
# side matches the concrete scope (e.g. `user:1`) on the other,
|
|
106
|
+
# and the concrete scope is what ends up in the result. Unlike
|
|
107
|
+
# #allowed, the same scopes are returned regardless of which set is
|
|
108
|
+
# the receiver, though the order may differ: scopes matched from the
|
|
109
|
+
# argument come first, followed by any scope only the receiver
|
|
110
|
+
# contributes (a concrete scope matched by a pattern in the argument).
|
|
111
|
+
#
|
|
112
|
+
# @param other The set of scopes to intersect with
|
|
113
|
+
def common(other)
|
|
114
|
+
other = self.class.from_array(to_array(other))
|
|
115
|
+
matched = other.select { |scope| exists?(scope) } +
|
|
116
|
+
select { |scope| other.exists?(scope) }
|
|
117
|
+
self.class.from_array(matched)
|
|
118
|
+
end
|
|
119
|
+
|
|
95
120
|
private
|
|
96
121
|
|
|
97
122
|
def dynamic_scopes_enabled?
|
|
@@ -113,6 +113,12 @@ module Doorkeeper
|
|
|
113
113
|
iat: @token.created_at.to_i,
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
# RFC 8707: include audience restriction when resource indicators are present
|
|
117
|
+
if @token.try(:resource).present?
|
|
118
|
+
aud = @token.resource.split
|
|
119
|
+
response[:aud] = aud.length == 1 ? aud.first : aud
|
|
120
|
+
end
|
|
121
|
+
|
|
116
122
|
# `token_type` (RFC 6749 §7.1) and `exp` describe the access token:
|
|
117
123
|
# a refresh token is not a Bearer credential and has no expiry of its
|
|
118
124
|
# own, so both are omitted (they are OPTIONAL per RFC 7662 §2.2) when
|
data/lib/doorkeeper/version.rb
CHANGED
data/lib/doorkeeper.rb
CHANGED
|
@@ -8,7 +8,9 @@ require "doorkeeper/engine"
|
|
|
8
8
|
module Doorkeeper
|
|
9
9
|
autoload :Errors, "doorkeeper/errors"
|
|
10
10
|
autoload :ClientAuthentication, "doorkeeper/client_authentication"
|
|
11
|
+
autoload :DocumentCache, "doorkeeper/document_cache"
|
|
11
12
|
autoload :GrantFlow, "doorkeeper/grant_flow"
|
|
13
|
+
autoload :HttpFetcher, "doorkeeper/http_fetcher"
|
|
12
14
|
autoload :OAuth, "doorkeeper/oauth"
|
|
13
15
|
autoload :Rake, "doorkeeper/rake"
|
|
14
16
|
autoload :Request, "doorkeeper/request"
|
|
@@ -58,6 +60,7 @@ module Doorkeeper
|
|
|
58
60
|
autoload :PasswordAccessTokenRequest, "doorkeeper/oauth/password_access_token_request"
|
|
59
61
|
autoload :PreAuthorization, "doorkeeper/oauth/pre_authorization"
|
|
60
62
|
autoload :RefreshTokenRequest, "doorkeeper/oauth/refresh_token_request"
|
|
63
|
+
autoload :ResourceIndicatorValidator, "doorkeeper/oauth/resource_indicator_validator"
|
|
61
64
|
autoload :Scopes, "doorkeeper/oauth/scopes"
|
|
62
65
|
autoload :Token, "doorkeeper/oauth/token"
|
|
63
66
|
autoload :TokenIntrospection, "doorkeeper/oauth/token_introspection"
|
|
@@ -68,6 +71,7 @@ module Doorkeeper
|
|
|
68
71
|
autoload :None, "doorkeeper/oauth/client_authentication/none"
|
|
69
72
|
autoload :ClientSecretBasic, "doorkeeper/oauth/client_authentication/client_secret_basic"
|
|
70
73
|
autoload :ClientSecretPost, "doorkeeper/oauth/client_authentication/client_secret_post"
|
|
74
|
+
autoload :PrivateKeyJwt, "doorkeeper/oauth/client_authentication/private_key_jwt"
|
|
71
75
|
end
|
|
72
76
|
|
|
73
77
|
module Authorization
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Doorkeeper
|
|
7
|
+
# Generates migration to add the access token reference column to the
|
|
8
|
+
# access grants table, enabling revocation of previously issued tokens
|
|
9
|
+
# when an authorization code is reused (RFC 6749 §4.1.2, §10.5).
|
|
10
|
+
#
|
|
11
|
+
class GrantReuseRevocationGenerator < ::Rails::Generators::Base
|
|
12
|
+
include ::Rails::Generators::Migration
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
desc "Support revoking issued tokens on authorization code reuse"
|
|
15
|
+
|
|
16
|
+
def self.next_migration_number(path)
|
|
17
|
+
ActiveRecord::Generators::Base.next_migration_number(path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def grant_reuse_revocation
|
|
21
|
+
return unless no_access_token_id_column?
|
|
22
|
+
|
|
23
|
+
migration_template(
|
|
24
|
+
"add_access_token_to_access_grants.rb.erb",
|
|
25
|
+
"db/migrate/add_access_token_to_access_grants.rb",
|
|
26
|
+
migration_version: migration_version,
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def migration_version
|
|
33
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def no_access_token_id_column?
|
|
37
|
+
!ActiveRecord::Base.connection.column_exists?(
|
|
38
|
+
:oauth_access_grants,
|
|
39
|
+
:access_token_id,
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Doorkeeper
|
|
7
|
+
# Generates migration with the `resource` column for access grants and
|
|
8
|
+
# access tokens, required for RFC 8707 Resource Indicators support.
|
|
9
|
+
#
|
|
10
|
+
class ResourceIndicatorsGenerator < ::Rails::Generators::Base
|
|
11
|
+
include ::Rails::Generators::Migration
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
desc "Add resource indicators support (RFC 8707) to Doorkeeper tables."
|
|
14
|
+
|
|
15
|
+
def resource_indicators
|
|
16
|
+
migration_template(
|
|
17
|
+
"enable_resource_indicators_migration.rb.erb",
|
|
18
|
+
"db/migrate/enable_resource_indicators.rb",
|
|
19
|
+
migration_version: migration_version,
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.next_migration_number(dirname)
|
|
24
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def migration_version
|
|
30
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddAccessTokenToAccessGrants < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
# Indexed: when a code is replayed, the revocation checks whether the
|
|
6
|
+
# issued token is still referenced by another grant, which filters
|
|
7
|
+
# oauth_access_grants by access_token_id.
|
|
8
|
+
add_reference :oauth_access_grants, :access_token, index: true
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class EnableResourceIndicators < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
add_column :oauth_access_grants, :resource, :text, null: true
|
|
6
|
+
add_column :oauth_access_tokens, :resource, :text, null: true
|
|
7
|
+
end
|
|
8
|
+
end
|