doorkeeper 5.9.5 → 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 +70 -3
- data/README.md +204 -0
- data/app/controllers/doorkeeper/applications_controller.rb +38 -9
- data/app/controllers/doorkeeper/authorizations_controller.rb +17 -3
- data/app/controllers/doorkeeper/metadata_controller.rb +20 -0
- data/app/controllers/doorkeeper/tokens_controller.rb +15 -2
- data/app/views/doorkeeper/authorizations/new.html.erb +24 -0
- data/config/locales/en.yml +2 -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 +52 -0
- data/lib/doorkeeper/client_authentication/method.rb +40 -0
- data/lib/doorkeeper/client_authentication/registry.rb +47 -0
- data/lib/doorkeeper/client_authentication/verified_credentials.rb +19 -0
- data/lib/doorkeeper/client_authentication.rb +102 -0
- data/lib/doorkeeper/config/option.rb +1 -1
- data/lib/doorkeeper/config/validations.rb +162 -1
- data/lib/doorkeeper/config.rb +161 -9
- data/lib/doorkeeper/document_cache.rb +81 -0
- data/lib/doorkeeper/errors.rb +49 -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 +81 -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/code.rb +10 -0
- data/lib/doorkeeper/oauth/authorization/token.rb +13 -2
- data/lib/doorkeeper/oauth/authorization/uri_builder.rb +11 -0
- data/lib/doorkeeper/oauth/authorization_code_request.rb +127 -3
- data/lib/doorkeeper/oauth/base_request.rb +1 -2
- data/lib/doorkeeper/oauth/client.rb +24 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +57 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_post.rb +33 -0
- data/lib/doorkeeper/oauth/client_authentication/none.rb +71 -0
- 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/code_response.rb +16 -2
- data/lib/doorkeeper/oauth/error_response.rb +11 -2
- data/lib/doorkeeper/oauth/helpers/uri_checker.rb +35 -23
- data/lib/doorkeeper/oauth/metadata_response.rb +165 -0
- data/lib/doorkeeper/oauth/password_access_token_request.rb +27 -1
- data/lib/doorkeeper/oauth/pre_authorization.rb +63 -8
- 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 +33 -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 +12 -4
- 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 +131 -6
- data/lib/generators/doorkeeper/templates/migration.rb.erb +13 -0
- metadata +52 -4
- data/lib/doorkeeper/oauth/client/credentials.rb +0 -71
- data/lib/doorkeeper/oauth/stateless_token.rb +0 -139
|
@@ -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
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
module OAuth
|
|
5
|
+
# OAuth 2.0 Authorization Server Metadata response as described in
|
|
6
|
+
# RFC 8414 (https://www.rfc-editor.org/rfc/rfc8414).
|
|
7
|
+
class MetadataResponse < BaseResponse
|
|
8
|
+
def initialize(base_url, url_builder)
|
|
9
|
+
super()
|
|
10
|
+
@base_url = base_url
|
|
11
|
+
@url_builder = url_builder
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def body
|
|
15
|
+
@body ||= begin
|
|
16
|
+
data = {
|
|
17
|
+
# presence: a blank issuer (e.g. an unset env var) must fall back
|
|
18
|
+
# to base_url, matching the issuer.present? condition below so the
|
|
19
|
+
# two fields cannot disagree.
|
|
20
|
+
issuer: issuer.presence || @base_url,
|
|
21
|
+
# Only advertise endpoints whose controllers are installed. A
|
|
22
|
+
# controller disabled through skip_controllers has no routes mapping,
|
|
23
|
+
# so endpoint_for resolves it to nil and it is dropped below.
|
|
24
|
+
authorization_endpoint: authorization_endpoint,
|
|
25
|
+
token_endpoint: token_endpoint,
|
|
26
|
+
revocation_endpoint: revocation_endpoint,
|
|
27
|
+
introspection_endpoint: (introspection_endpoint if introspection_enabled?),
|
|
28
|
+
scopes_supported: scopes_supported,
|
|
29
|
+
response_types_supported: response_types_supported,
|
|
30
|
+
response_modes_supported: response_modes_supported,
|
|
31
|
+
grant_types_supported: grant_types_supported,
|
|
32
|
+
token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported,
|
|
33
|
+
code_challenge_methods_supported: code_challenge_methods_supported,
|
|
34
|
+
# RFC 9207: true only when an issuer is configured, matching the
|
|
35
|
+
# condition under which the iss parameter is emitted. false survives
|
|
36
|
+
# the compaction below, so it is advertised explicitly.
|
|
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?,
|
|
40
|
+
}
|
|
41
|
+
data.compact!
|
|
42
|
+
|
|
43
|
+
# userinfo_endpoint is intentionally advertised as null for backwards
|
|
44
|
+
# compatibility; it is meant to be populated through custom_metadata
|
|
45
|
+
# (e.g. by an OIDC extension), so it must survive the compaction above.
|
|
46
|
+
data[:userinfo_endpoint] = userinfo_endpoint
|
|
47
|
+
|
|
48
|
+
data.merge(custom_metadata)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def status
|
|
53
|
+
:ok
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def headers
|
|
57
|
+
{
|
|
58
|
+
"Cache-Control" => "public",
|
|
59
|
+
"Content-Type" => "application/json; charset=utf-8",
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def config
|
|
66
|
+
@config ||= Doorkeeper.configuration
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def url_for(**args)
|
|
70
|
+
@url_builder.call(**args)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Resolve the URL for the given route group, or nil when the group has no
|
|
74
|
+
# routes mapping (i.e. the controller was disabled via skip_controllers).
|
|
75
|
+
def endpoint_for(group, action:)
|
|
76
|
+
mapping = Doorkeeper::Rails::Routes.mapping[group]
|
|
77
|
+
return unless mapping
|
|
78
|
+
|
|
79
|
+
# The leading slash anchors the controller name: without it, Rails
|
|
80
|
+
# resolves the name relative to the current (doorkeeper/metadata)
|
|
81
|
+
# namespace whenever their segment depths differ, e.g. a configured
|
|
82
|
+
# `controllers tokens: "custom_tokens"` would be looked up as
|
|
83
|
+
# "doorkeeper/custom_tokens" and raise ActionController::UrlGenerationError.
|
|
84
|
+
url_for(controller: "/#{mapping[:controllers]}", action: action)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def custom_metadata
|
|
88
|
+
config.custom_metadata.symbolize_keys
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def issuer
|
|
92
|
+
config.issuer
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def authorization_endpoint
|
|
96
|
+
endpoint_for(:authorizations, action: "new")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def token_endpoint
|
|
100
|
+
endpoint_for(:tokens, action: "create")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def userinfo_endpoint
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def revocation_endpoint
|
|
108
|
+
endpoint_for(:tokens, action: "revoke")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def introspection_endpoint
|
|
112
|
+
endpoint_for(:tokens, action: "introspect")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def introspection_enabled?
|
|
116
|
+
Doorkeeper.configured? &&
|
|
117
|
+
!config.allow_token_introspection.is_a?(FalseClass)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def scopes_supported
|
|
121
|
+
config.scopes.to_a
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def response_types_supported
|
|
125
|
+
config.authorization_response_types
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def response_modes_supported
|
|
129
|
+
config.authorization_response_flows.flat_map(&:response_mode_matches).uniq
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def grant_types_supported
|
|
133
|
+
# RFC 8414 lists token-endpoint grant type values (e.g.
|
|
134
|
+
# "authorization_code", "refresh_token"), so derive them from the
|
|
135
|
+
# flows that actually handle a grant_type and use their grant_type
|
|
136
|
+
# value rather than the configured flow name. This drops
|
|
137
|
+
# response-type-only flows such as :implicit and reports the real
|
|
138
|
+
# grant type for flows whose name differs from it (e.g. a URN).
|
|
139
|
+
config.token_grant_types
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The resolved methods (rather than the raw client_authentication names)
|
|
143
|
+
# reflect what the server actually accepts: unregistered names are
|
|
144
|
+
# dropped by the resolver, and a deprecated client_credentials-only
|
|
145
|
+
# configuration is honored as the source of truth. Legacy callable
|
|
146
|
+
# extractors have no registered method name a client could use, so they
|
|
147
|
+
# are not advertised.
|
|
148
|
+
def token_endpoint_auth_methods_supported
|
|
149
|
+
config.client_authentication_methods.filter_map do |method|
|
|
150
|
+
method.name.to_s if Doorkeeper::ClientAuthentication.get(method.name)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def code_challenge_methods_supported
|
|
155
|
+
config.pkce_code_challenge_methods_supported
|
|
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
|
|
163
|
+
end
|
|
164
|
+
end
|
|
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
|
|
@@ -7,20 +7,21 @@ module Doorkeeper
|
|
|
7
7
|
|
|
8
8
|
validate :client_id, error: Errors::InvalidRequest
|
|
9
9
|
validate :client, error: Errors::InvalidClient
|
|
10
|
-
validate :client_supports_grant_flow, error: Errors::UnauthorizedClient
|
|
11
|
-
validate :resource_owner_authorize_for_client, error: Errors::InvalidClient
|
|
12
10
|
validate :redirect_uri, error: Errors::InvalidRedirectUri
|
|
11
|
+
validate :client_supports_grant_flow, error: Errors::UnauthorizedClient
|
|
12
|
+
validate :resource_owner_authorize_for_client, error: Errors::AccessDenied
|
|
13
13
|
validate :params, error: Errors::InvalidRequest
|
|
14
14
|
validate :response_type, error: Errors::UnsupportedResponseType
|
|
15
15
|
validate :response_mode, error: Errors::UnsupportedResponseMode
|
|
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?
|
|
@@ -49,13 +51,21 @@ module Doorkeeper
|
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def error_response
|
|
54
|
+
# RFC 9207: authorization error responses advertise the issuer. Passing
|
|
55
|
+
# the configured issuer here (nil when unset) scopes the iss parameter
|
|
56
|
+
# to the authorization endpoint only.
|
|
52
57
|
if error == Errors::InvalidRequest
|
|
53
58
|
OAuth::InvalidRequestResponse.from_request(
|
|
54
59
|
self,
|
|
55
60
|
response_on_fragment: response_on_fragment?,
|
|
61
|
+
issuer: Doorkeeper.config.issuer,
|
|
56
62
|
)
|
|
57
63
|
else
|
|
58
|
-
OAuth::ErrorResponse.from_request(
|
|
64
|
+
OAuth::ErrorResponse.from_request(
|
|
65
|
+
self,
|
|
66
|
+
response_on_fragment: response_on_fragment?,
|
|
67
|
+
issuer: Doorkeeper.config.issuer,
|
|
68
|
+
)
|
|
59
69
|
end
|
|
60
70
|
end
|
|
61
71
|
|
|
@@ -76,7 +86,7 @@ module Doorkeeper
|
|
|
76
86
|
if client_scopes.blank?
|
|
77
87
|
server.default_scopes.to_s
|
|
78
88
|
else
|
|
79
|
-
server.default_scopes.
|
|
89
|
+
server.default_scopes.common(client_scopes).to_s
|
|
80
90
|
end
|
|
81
91
|
end
|
|
82
92
|
|
|
@@ -115,7 +125,21 @@ module Doorkeeper
|
|
|
115
125
|
:scope
|
|
116
126
|
end
|
|
117
127
|
|
|
118
|
-
@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)
|
|
119
143
|
end
|
|
120
144
|
|
|
121
145
|
def validate_response_type
|
|
@@ -147,7 +171,14 @@ module Doorkeeper
|
|
|
147
171
|
|
|
148
172
|
def validate_code_challenge
|
|
149
173
|
return true unless Doorkeeper.config.force_pkce?
|
|
150
|
-
|
|
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?
|
|
151
182
|
return true if code_challenge.present?
|
|
152
183
|
|
|
153
184
|
@invalid_request_reason = :invalid_code_challenge
|
|
@@ -161,6 +192,21 @@ module Doorkeeper
|
|
|
161
192
|
(code_challenge_method.present? && Doorkeeper.config.pkce_code_challenge_methods_supported.include?(code_challenge_method))
|
|
162
193
|
end
|
|
163
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
|
+
|
|
164
210
|
def response_on_fragment?
|
|
165
211
|
return response_type == "token" if response_mode.nil?
|
|
166
212
|
|
|
@@ -171,6 +217,15 @@ module Doorkeeper
|
|
|
171
217
|
response_type == "code" ? AUTHORIZATION_CODE : IMPLICIT
|
|
172
218
|
end
|
|
173
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
|
+
|
|
174
229
|
def pre_auth_hash
|
|
175
230
|
{
|
|
176
231
|
client_id: client.uid,
|
|
@@ -180,7 +235,7 @@ module Doorkeeper
|
|
|
180
235
|
scope: scope,
|
|
181
236
|
client_name: client.name,
|
|
182
237
|
status: I18n.t("doorkeeper.pre_authorization.status"),
|
|
183
|
-
}
|
|
238
|
+
}.reverse_merge(custom_access_token_attributes.symbolize_keys)
|
|
184
239
|
end
|
|
185
240
|
end
|
|
186
241
|
end
|
|
@@ -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?
|