doorkeeper 5.9.3 → 5.9.9
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 +22 -3
- data/app/controllers/doorkeeper/authorizations_controller.rb +33 -3
- data/app/controllers/doorkeeper/authorized_applications_controller.rb +23 -0
- data/config/locales/en.yml +1 -0
- data/lib/doorkeeper/config.rb +7 -1
- data/lib/doorkeeper/errors.rb +13 -0
- data/lib/doorkeeper/helpers/controller.rb +2 -1
- data/lib/doorkeeper/models/access_token_mixin.rb +8 -3
- data/lib/doorkeeper/oauth/client/credentials.rb +57 -5
- data/lib/doorkeeper/oauth/client_credentials/creator.rb +4 -1
- data/lib/doorkeeper/oauth/helpers/uri_checker.rb +14 -0
- data/lib/doorkeeper/oauth/pre_authorization.rb +27 -0
- data/lib/doorkeeper/oauth/token.rb +95 -2
- data/lib/doorkeeper/orm/active_record/redirect_uri_validator.rb +5 -1
- data/lib/doorkeeper/request.rb +19 -7
- data/lib/doorkeeper/version.rb +1 -1
- data/lib/generators/doorkeeper/templates/initializer.rb +13 -2
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c4d0c4d775062d003452c97290c5fb4308812e17590946b9f8db9922e40f9d7
|
|
4
|
+
data.tar.gz: 7af434e11413a59e94cf12edd6ddcb088940110fabbd1174f0bacb2869a254da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1029aa2d0ff981b3d114f656f5b8d17308a946edd9466e7fe93d93fa5b6d1ba3c6873ba6cf628eab86ccb2f50b14507f272d61a0af829f2184963908941bb91c
|
|
7
|
+
data.tar.gz: 5b474f0a07ba2785acdbadca5d874a0b9e50955bebd0318499f84710cb06b0d1e8d58419ac4b889045e504011ddc88398ffea0c345434df291f0b43cf60fb3d2
|
data/CHANGELOG.md
CHANGED
|
@@ -5,9 +5,29 @@ upgrade guides.
|
|
|
5
5
|
|
|
6
6
|
User-visible changes worth mentioning.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## 5.9.9
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- Fix: `AuthorizedApplicationsController` now answers `401 Unauthorized` instead of running with a `nil` resource owner, which listed and revoked every token that has no resource owner — the ones the client credentials flow issues. Affected host applications are those whose `resource_owner_authenticator` answers `nil` without halting the request itself; the generated initializer's example redirects and is not affected.
|
|
11
|
+
- Fix: `AuthorizationsController#destroy` now validates the client and redirect URI before producing the deny response, and renders — never redirects — when validation fails. Previously the deny path performed no OAuth-layer validation at all, allowing an open redirect to an attacker-controlled origin with the OAuth `state` attached. Also reject unregistered `response_type` values on the authorization endpoint rather than resolving them through the `constantize` fallback.
|
|
12
|
+
- Fix: refuse redirect URIs with a script scheme (`javascript`, `vbscript`, `data`) both when an application is registered and at authorization time, regardless of `forbid_redirect_uri`. Such a URI is never a legitimate redirection endpoint, and with `response_mode=form_post` it became the action of the auto-submitting form the authorization server renders on its own origin. Already stored records with such a URI are now refused with `invalid_redirect_uri` before the consent screen is shown.
|
|
13
|
+
|
|
14
|
+
## 5.9.8
|
|
15
|
+
|
|
16
|
+
- [#1938] Fix: a request body ActionDispatch cannot parse (malformed JSON under a JSON content type, say) no longer raises `ActionDispatch::Http::Parameters::ParseError` out of `Doorkeeper::OAuth::Token.from_request` and `doorkeeper_token`. Since 5.9.7 the RFC 6750 §2 multi-method check read the body on every request, so such a request raised even when it carried a valid Bearer header. The body is now treated as carrying no token, the same way ActionDispatch's own `#filtered_parameters` treats that error.
|
|
17
|
+
|
|
18
|
+
## 5.9.7
|
|
19
|
+
|
|
20
|
+
- Refuse requests that transmit an access token by more than one method (RFC 6750 §2), instead of silently authorizing with the first configured `access_token_methods` entry that matched and discarding the other tokens. Such a request now fails closed as carrying no usable token (401 `invalid_token`); no calling contract changes. The form-encoded body (§2.2) and the URI query (§2.3) count as two methods even though Rails and Rack merge them into a single `params` hash. The same token repeated across two methods is refused too — §2 forbids the second method, not a disagreement between the two — and a custom callable extractor in `access_token_methods` keeps the historical first-wins behavior and is never invoked more than once. (The strict `invalid_request` (400) answer §3.1 prescribes ships with Doorkeeper 6.0.)
|
|
21
|
+
- [#1925] Internal: pin the development dependency on `json` below 3.0. json 3 removed the positional options Hash from `JSON.parse` and the `quirks_mode` option from `JSON.generate`, both of which Active Support still uses, so the suite could not run on any supported Rails version.
|
|
22
|
+
|
|
23
|
+
## 5.9.6
|
|
24
|
+
|
|
25
|
+
- Reject requests that present more than one client identity (e.g. an `Authorization: Basic` header for one client and a `client_id` parameter naming another) with an `invalid_request` error, instead of authenticating the first extracted identity and silently discarding the other one. A `client_id` sent alongside another authentication method keeps working when it identifies the same client (RFC 7521 §4.2). Like the RFC 6749 §2.3 check released in 5.9.5, this validation does not apply when `client_credentials` is configured with a callable extractor, since the credentials the remaining extractors would return are never evaluated — the `client_credentials` option documents that now.
|
|
26
|
+
|
|
27
|
+
## 5.9.5
|
|
28
|
+
|
|
29
|
+
- Reject requests that authenticate the client with more than one method (RFC 6749 §2.3) with an `invalid_request` error, instead of silently authenticating with the first method that matched and discarding the other credentials.
|
|
30
|
+
- [#1853] Fix `reuse_access_token` reusing a token that was created with `custom_access_token_attributes` values when the new request doesn't specify any custom attributes. Such requests now only match tokens without custom attributes.
|
|
11
31
|
|
|
12
32
|
## 5.9.3
|
|
13
33
|
|
|
@@ -57,7 +77,6 @@ User-visible changes worth mentioning.
|
|
|
57
77
|
- [#1779] Only lock previous access token model when creating a new token from its refresh token if revoke_previous_refresh_token_on_use is false
|
|
58
78
|
- [#1778] Ensure that token revocation is idempotent by checking that that token has not already been revoked before revoking.
|
|
59
79
|
|
|
60
|
-
|
|
61
80
|
## 5.8.2
|
|
62
81
|
|
|
63
82
|
- [#1755] Fix the error message for force_pkce
|
|
@@ -16,7 +16,13 @@ module Doorkeeper
|
|
|
16
16
|
redirect_or_render(authorize_response)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# The deny path answers a request the client sent, so it is subject to the
|
|
20
|
+
# same client and redirect URI validation as #create: without it an
|
|
21
|
+
# unregistered redirect_uri was honoured and the user-agent redirected to
|
|
22
|
+
# it, carrying the OAuth `state` to an origin of the caller's choosing.
|
|
19
23
|
def destroy
|
|
24
|
+
return if refuse_invalid_client?
|
|
25
|
+
|
|
20
26
|
redirect_or_render(authorization.deny)
|
|
21
27
|
rescue Doorkeeper::Errors::InvalidTokenStrategy => e
|
|
22
28
|
error_response = get_error_response_from_exception(e)
|
|
@@ -45,13 +51,37 @@ module Doorkeeper
|
|
|
45
51
|
|
|
46
52
|
if Doorkeeper.configuration.redirect_on_errors? && pre_auth.error_response.redirectable?
|
|
47
53
|
redirect_or_render(pre_auth.error_response)
|
|
48
|
-
elsif Doorkeeper.configuration.api_only
|
|
49
|
-
render json: pre_auth.error_response.body, status: pre_auth.error_response.status
|
|
50
54
|
else
|
|
51
|
-
|
|
55
|
+
render_error_response(pre_auth.error_response)
|
|
52
56
|
end
|
|
53
57
|
end
|
|
54
58
|
|
|
59
|
+
def render_error_response(error_response)
|
|
60
|
+
if Doorkeeper.configuration.api_only
|
|
61
|
+
render json: error_response.body, status: error_response.status
|
|
62
|
+
else
|
|
63
|
+
render :error, locals: { error_response: error_response }, status: error_response.status
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Renders the error when the request's client_id or redirect_uri is missing
|
|
68
|
+
# or invalid, and answers whether the request was refused. Errors are
|
|
69
|
+
# rendered, never redirected, regardless of `handle_auth_errors :redirect`:
|
|
70
|
+
# no failure this check can produce leaves a redirect target worth
|
|
71
|
+
# trusting. Either the client failed first, so the redirect URI was never
|
|
72
|
+
# reached, or the redirect URI is itself the invalid one - and RFC 6749
|
|
73
|
+
# Section 3.1.2.4 forbids redirecting the user-agent to an invalid
|
|
74
|
+
# redirection URI.
|
|
75
|
+
def refuse_invalid_client?
|
|
76
|
+
return false if pre_auth.client_valid?
|
|
77
|
+
|
|
78
|
+
error_response = pre_auth.error_response
|
|
79
|
+
error_response.raise_exception! if Doorkeeper.config.raise_on_errors?
|
|
80
|
+
|
|
81
|
+
render_error_response(error_response)
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
55
85
|
def can_authorize_response?
|
|
56
86
|
Doorkeeper.config.custom_access_token_attributes.empty? && pre_auth.client.application.confidential? && matching_token?
|
|
57
87
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Doorkeeper
|
|
4
4
|
class AuthorizedApplicationsController < Doorkeeper::ApplicationController
|
|
5
5
|
before_action :authenticate_resource_owner!
|
|
6
|
+
before_action :validate_resource_owner
|
|
6
7
|
|
|
7
8
|
def index
|
|
8
9
|
@applications = Doorkeeper.config.application_model.authorized_for(current_resource_owner)
|
|
@@ -29,5 +30,27 @@ module Doorkeeper
|
|
|
29
30
|
format.json { head :no_content }
|
|
30
31
|
end
|
|
31
32
|
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# `authenticate_resource_owner!` hands the request to the host
|
|
37
|
+
# application's `resource_owner_authenticator` block, which is expected to
|
|
38
|
+
# halt the request itself — redirect to a sign-in page, raise, render —
|
|
39
|
+
# when nobody is signed in. A block that merely answers nil halts nothing:
|
|
40
|
+
# the library default (no block configured) logs a warning and answers
|
|
41
|
+
# nil, and so does any block written as a bare lookup such as
|
|
42
|
+
# `User.find_by(id: session[:user_id])`.
|
|
43
|
+
#
|
|
44
|
+
# Both actions here scope their work to `current_resource_owner`, and a
|
|
45
|
+
# nil owner is not "no scope" — it is the scope of the records that have
|
|
46
|
+
# no resource owner, which is what the client credentials flow issues.
|
|
47
|
+
# #index would list every application holding one and #destroy would
|
|
48
|
+
# revoke them, for a caller that never authenticated. So refuse the
|
|
49
|
+
# request rather than treat "nobody" as an owner. Only this controller is
|
|
50
|
+
# guarded; `authenticate_resource_owner!` itself is left alone because
|
|
51
|
+
# AuthorizationsController shapes its own response around the owner.
|
|
52
|
+
def validate_resource_owner
|
|
53
|
+
head :unauthorized unless current_resource_owner
|
|
54
|
+
end
|
|
32
55
|
end
|
|
33
56
|
end
|
data/config/locales/en.yml
CHANGED
|
@@ -97,6 +97,7 @@ en:
|
|
|
97
97
|
missing_param: 'Missing required parameter: %{value}.'
|
|
98
98
|
request_not_authorized: 'Request needs to be authorized. Required parameter for authorizing the request is missing or invalid.'
|
|
99
99
|
invalid_code_challenge: 'Code challenge is required.'
|
|
100
|
+
multiple_client_auth_methods: 'The request utilizes more than one mechanism for authenticating the client.'
|
|
100
101
|
invalid_redirect_uri: "The requested redirect URI is malformed or doesn't match the client redirect URI."
|
|
101
102
|
unauthorized_client: 'The client is not authorized to perform this request using this method.'
|
|
102
103
|
access_denied: 'The resource owner or authorization server denied the request.'
|
data/lib/doorkeeper/config.rb
CHANGED
|
@@ -69,6 +69,10 @@ module Doorkeeper
|
|
|
69
69
|
# falls back to the `:client_id` and `:client_secret` params from the
|
|
70
70
|
# `params` object.
|
|
71
71
|
#
|
|
72
|
+
# NOTE: a list that includes a callable extractor opts out of the RFC 6749
|
|
73
|
+
# §2.3 validation applied to the symbol extractors — see
|
|
74
|
+
# +Doorkeeper::OAuth::Client::Credentials.from_request+.
|
|
75
|
+
#
|
|
72
76
|
# @param methods [Array] Define client credentials
|
|
73
77
|
def client_credentials(*methods)
|
|
74
78
|
@config.instance_variable_set(:@client_credentials_methods, methods)
|
|
@@ -325,7 +329,9 @@ module Doorkeeper
|
|
|
325
329
|
option :allow_grant_flow_for_client, default: ->(_grant_flow, _client) { true }
|
|
326
330
|
|
|
327
331
|
# Allows to forbid specific Application redirect URI's by custom rules.
|
|
328
|
-
# Doesn't forbid any URI by default.
|
|
332
|
+
# Doesn't forbid any URI by default. Redirect URIs with a script scheme
|
|
333
|
+
# (`javascript`, `vbscript`, `data`) are always rejected, regardless of
|
|
334
|
+
# this option.
|
|
329
335
|
#
|
|
330
336
|
# @param forbid_redirect_uri [Proc] Block or any object respond to #call
|
|
331
337
|
#
|
data/lib/doorkeeper/errors.rb
CHANGED
|
@@ -24,6 +24,19 @@ module Doorkeeper
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Raised when a request uses more than one client authentication method,
|
|
28
|
+
# which RFC 6749 §2.3 explicitly forbids ("The client MUST NOT use more
|
|
29
|
+
# than one authentication method in each request").
|
|
30
|
+
class MultipleClientAuthMethods < DoorkeeperError
|
|
31
|
+
def type
|
|
32
|
+
:invalid_request
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reason
|
|
36
|
+
:multiple_client_auth_methods
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
27
40
|
class MissingRequiredParameter < DoorkeeperError
|
|
28
41
|
attr_reader :missing_param
|
|
29
42
|
|
|
@@ -56,7 +56,8 @@ module Doorkeeper
|
|
|
56
56
|
OAuth::InvalidRequestResponse.new(
|
|
57
57
|
name: exception.type,
|
|
58
58
|
state: params[:state],
|
|
59
|
-
missing_param: exception.missing_param,
|
|
59
|
+
missing_param: exception.try(:missing_param),
|
|
60
|
+
reason: exception.try(:reason),
|
|
60
61
|
)
|
|
61
62
|
else
|
|
62
63
|
OAuth::ErrorResponse.new(name: exception.type, state: params[:state])
|
|
@@ -89,7 +89,8 @@ module Doorkeeper
|
|
|
89
89
|
# @param custom_attributes [Nilable Hash]
|
|
90
90
|
# A nil value, or hash with keys corresponding to the custom attributes
|
|
91
91
|
# configured with the `custom_access_token_attributes` config option.
|
|
92
|
-
# A nil value will ignore custom attributes
|
|
92
|
+
# A nil value will ignore custom attributes, while an empty hash will
|
|
93
|
+
# only match tokens that have no custom attributes set.
|
|
93
94
|
#
|
|
94
95
|
# @return [Doorkeeper::AccessToken, nil] Access Token instance or
|
|
95
96
|
# nil if matching record was not found
|
|
@@ -124,7 +125,8 @@ module Doorkeeper
|
|
|
124
125
|
# @param custom_attributes [Nilable Hash]
|
|
125
126
|
# A nil value, or hash with keys corresponding to the custom attributes
|
|
126
127
|
# configured with the `custom_access_token_attributes` config option.
|
|
127
|
-
# A nil value will ignore custom attributes
|
|
128
|
+
# A nil value will ignore custom attributes, while an empty hash will
|
|
129
|
+
# only match tokens that have no custom attributes set.
|
|
128
130
|
#
|
|
129
131
|
# @return [Doorkeeper::AccessToken, nil] Access Token instance or
|
|
130
132
|
# nil if matching record was not found
|
|
@@ -220,7 +222,10 @@ module Doorkeeper
|
|
|
220
222
|
scopes = Doorkeeper::OAuth::Scopes.from_string(scopes) if scopes.is_a?(String)
|
|
221
223
|
|
|
222
224
|
if Doorkeeper.config.reuse_access_token
|
|
223
|
-
|
|
225
|
+
# An empty hash must stay distinct from nil here: nil ignores custom
|
|
226
|
+
# attributes when matching, while an empty hash only matches tokens
|
|
227
|
+
# that have no custom attributes set.
|
|
228
|
+
custom_attributes = extract_custom_attributes(token_attributes)
|
|
224
229
|
access_token = matching_token_for(
|
|
225
230
|
application, resource_owner, scopes, custom_attributes: custom_attributes, include_expired: false,
|
|
226
231
|
)
|
|
@@ -5,12 +5,48 @@ module Doorkeeper
|
|
|
5
5
|
class Client
|
|
6
6
|
Credentials = Struct.new(:uid, :secret) do
|
|
7
7
|
class << self
|
|
8
|
+
# Extracts the client credentials from the request using the
|
|
9
|
+
# configured extraction methods. The first method that yields
|
|
10
|
+
# credentials wins, as it always has.
|
|
11
|
+
#
|
|
12
|
+
# Raises Errors::MultipleClientAuthMethods when the request
|
|
13
|
+
# authenticates the client with more than one method, or presents
|
|
14
|
+
# more than one client identity, which RFC 6749 §2.3 forbids ("The
|
|
15
|
+
# client MUST NOT use more than one authentication method in each
|
|
16
|
+
# request").
|
|
8
17
|
def from_request(request, *credentials_methods)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
# Callable extractors are opaque: they may legitimately overlap the
|
|
19
|
+
# built-in methods they are configured with, and evaluating all of
|
|
20
|
+
# them would also break the existing contract that the extractors
|
|
21
|
+
# after the matching one are not called. Such configurations keep
|
|
22
|
+
# the historical behaviour untouched, including the fact that a
|
|
23
|
+
# request authenticating with more than one method, or presenting
|
|
24
|
+
# more than one client identity, resolves to the first extracted
|
|
25
|
+
# credentials instead of being rejected: the extractor that would
|
|
26
|
+
# have surfaced the second identity is never evaluated. The
|
|
27
|
+
# +client_credentials+ configuration option documents this.
|
|
28
|
+
return first_from_request(request, credentials_methods) unless credentials_methods.all?(Symbol)
|
|
29
|
+
|
|
30
|
+
credentials = credentials_methods.filter_map { |method| extract(request, method) }
|
|
31
|
+
|
|
32
|
+
# Only credentials carrying a secret authenticate the client. A bare
|
|
33
|
+
# uid is a public client identifying itself (RFC 6749 §2.3 "none"),
|
|
34
|
+
# not an authentication method of its own, so it doesn't count
|
|
35
|
+
# towards the single-method rule — RFC 7521 §4.2, for example,
|
|
36
|
+
# explicitly allows a client_id next to another authentication
|
|
37
|
+
# method.
|
|
38
|
+
raise Errors::MultipleClientAuthMethods if credentials.count { |c| c.secret.present? } > 1
|
|
39
|
+
|
|
40
|
+
# §4.2 allows that bare client_id because it identifies the *same*
|
|
41
|
+
# client as the authentication method it accompanies. Credentials
|
|
42
|
+
# naming two different clients are two client identities in one
|
|
43
|
+
# request and authenticate neither: without this check the first
|
|
44
|
+
# extracted uid would win and the other identity would be silently
|
|
45
|
+
# discarded, so the caller could authenticate as one client while
|
|
46
|
+
# the request asks to act as another.
|
|
47
|
+
raise Errors::MultipleClientAuthMethods if credentials.uniq(&:uid).size > 1
|
|
48
|
+
|
|
49
|
+
credentials.first
|
|
14
50
|
end
|
|
15
51
|
|
|
16
52
|
def from_params(request)
|
|
@@ -23,6 +59,22 @@ module Doorkeeper
|
|
|
23
59
|
Base64.decode64(Regexp.last_match(1)).split(/:/, 2)
|
|
24
60
|
end
|
|
25
61
|
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def first_from_request(request, credentials_methods)
|
|
66
|
+
credentials_methods.inject(nil) do |_, method|
|
|
67
|
+
credentials = extract(request, method)
|
|
68
|
+
break credentials if credentials
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns the credentials the given method extracts from the request,
|
|
73
|
+
# or nil when it extracts none.
|
|
74
|
+
def extract(request, method)
|
|
75
|
+
method = self.method(method) if method.is_a?(Symbol)
|
|
76
|
+
Credentials.new(*method.call(request)).presence
|
|
77
|
+
end
|
|
26
78
|
end
|
|
27
79
|
|
|
28
80
|
# Public clients may have their secret blank, but "credentials" are
|
|
@@ -45,8 +45,11 @@ module Doorkeeper
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def find_active_existing_token_for(client, scopes, attributes)
|
|
48
|
+
# An empty hash must stay distinct from nil here: nil ignores custom
|
|
49
|
+
# attributes when matching, while an empty hash only matches tokens
|
|
50
|
+
# that have no custom attributes set.
|
|
48
51
|
custom_attributes = Doorkeeper.config.access_token_model
|
|
49
|
-
.extract_custom_attributes(attributes)
|
|
52
|
+
.extract_custom_attributes(attributes)
|
|
50
53
|
Doorkeeper.config.access_token_model.matching_token_for(
|
|
51
54
|
client, nil, scopes, custom_attributes: custom_attributes, include_expired: false,
|
|
52
55
|
)
|
|
@@ -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
|
|
|
@@ -62,10 +71,15 @@ module Doorkeeper
|
|
|
62
71
|
|
|
63
72
|
def self.valid_scheme?(uri)
|
|
64
73
|
return false if uri.scheme.blank?
|
|
74
|
+
return false if script_scheme?(uri)
|
|
65
75
|
|
|
66
76
|
%w[localhost].exclude?(uri.scheme)
|
|
67
77
|
end
|
|
68
78
|
|
|
79
|
+
def self.script_scheme?(uri)
|
|
80
|
+
SCRIPT_SCHEMES.include?(uri.scheme.to_s.downcase)
|
|
81
|
+
end
|
|
82
|
+
|
|
69
83
|
def self.hypertext_scheme?(uri)
|
|
70
84
|
%w[http https].include?(uri.scheme)
|
|
71
85
|
end
|
|
@@ -5,6 +5,14 @@ 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, and none of their failures leaves a
|
|
10
|
+
# redirect target worth trusting: RFC 6749 Section 4.1.2.1 (Section
|
|
11
|
+
# 4.2.2.1 for the implicit flow) asks for the resource owner to be
|
|
12
|
+
# informed when the client_id is missing or invalid, and Section 3.1.2.4
|
|
13
|
+
# asks the same for the redirect URI.
|
|
14
|
+
CLIENT_VALIDATIONS = %i[client_id client redirect_uri].freeze
|
|
15
|
+
|
|
8
16
|
validate :client_id, error: Errors::InvalidRequest
|
|
9
17
|
validate :client, error: Errors::InvalidClient
|
|
10
18
|
validate :client_supports_grant_flow, error: Errors::UnauthorizedClient
|
|
@@ -40,6 +48,25 @@ module Doorkeeper
|
|
|
40
48
|
valid?
|
|
41
49
|
end
|
|
42
50
|
|
|
51
|
+
# Runs only CLIENT_VALIDATIONS, in declared order, so a request naming an
|
|
52
|
+
# unknown client or an invalid redirect URI can be refused on its own -
|
|
53
|
+
# the validations in between (client_supports_grant_flow,
|
|
54
|
+
# resource_owner_authorize_for_client) answer a different question and
|
|
55
|
+
# are left to a full #validate run.
|
|
56
|
+
def client_valid?
|
|
57
|
+
@error = nil
|
|
58
|
+
@missing_param = nil
|
|
59
|
+
|
|
60
|
+
self.class.validations.each do |validation|
|
|
61
|
+
next unless CLIENT_VALIDATIONS.include?(validation[:attribute])
|
|
62
|
+
|
|
63
|
+
@error = validation[:options][:error] unless send("validate_#{validation[:attribute]}")
|
|
64
|
+
break if @error
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@error.nil?
|
|
68
|
+
end
|
|
69
|
+
|
|
43
70
|
def scopes
|
|
44
71
|
Scopes.from_string(scope)
|
|
45
72
|
end
|
|
@@ -3,8 +3,54 @@
|
|
|
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. Refusal answers
|
|
34
|
+
# nil: the caller is told the request carries no usable token and
|
|
35
|
+
# fails closed with the invalid_token (401) response, keeping every
|
|
36
|
+
# calling contract on this stable branch intact — the invalid_request
|
|
37
|
+
# (400) that §3.1 strictly prescribes needs a new error and render
|
|
38
|
+
# path, so it ships with 6.0 only.
|
|
39
|
+
#
|
|
40
|
+
# Only the built-in extractors — symbols naming methods on this class,
|
|
41
|
+
# all of them side-effect-free reads of the request — take part in
|
|
42
|
+
# that check. A custom callable extractor is a configuration adapter
|
|
43
|
+
# rather than a transmission method: it keeps the historical
|
|
44
|
+
# first-wins selection and is never invoked more than once, the same
|
|
45
|
+
# exemption client authentication gives its legacy callable
|
|
46
|
+
# extractors (Request#validate_client_authentication!).
|
|
7
47
|
def from_request(request, *methods)
|
|
48
|
+
used = methods.sum do |method|
|
|
49
|
+
method.is_a?(Symbol) ? transmission_methods_used(request, method) : 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
return if used > 1
|
|
53
|
+
|
|
8
54
|
methods.inject(nil) do |_, method|
|
|
9
55
|
method = self.method(method) if method.is_a?(Symbol)
|
|
10
56
|
credentials = method.call(request)
|
|
@@ -23,11 +69,11 @@ module Doorkeeper
|
|
|
23
69
|
end
|
|
24
70
|
|
|
25
71
|
def from_access_token_param(request)
|
|
26
|
-
request
|
|
72
|
+
parameters(request)[:access_token]
|
|
27
73
|
end
|
|
28
74
|
|
|
29
75
|
def from_bearer_param(request)
|
|
30
|
-
request
|
|
76
|
+
parameters(request)[:bearer_token]
|
|
31
77
|
end
|
|
32
78
|
|
|
33
79
|
def from_bearer_authorization(request)
|
|
@@ -44,6 +90,53 @@ module Doorkeeper
|
|
|
44
90
|
|
|
45
91
|
private
|
|
46
92
|
|
|
93
|
+
# How many transmission methods the given built-in extractor finds a
|
|
94
|
+
# token in. Usually one, or none — but for the parameter extractors
|
|
95
|
+
# above it is the body and the query counted separately, since the
|
|
96
|
+
# parameter hash collapsed them into the single value the extractor
|
|
97
|
+
# reads. That value is counted on its own only when neither raw source
|
|
98
|
+
# explains it, so a token reaching the extractor by some other route —
|
|
99
|
+
# an :access_token path segment, or a host that overrode the extractor
|
|
100
|
+
# — is still counted exactly once rather than twice.
|
|
101
|
+
def transmission_methods_used(request, method)
|
|
102
|
+
value = self.method(method).call(request).presence
|
|
103
|
+
|
|
104
|
+
parameter = PARAMETER_EXTRACTORS[method]
|
|
105
|
+
return value ? 1 : 0 unless parameter
|
|
106
|
+
|
|
107
|
+
sources = parameter_sources(request, parameter)
|
|
108
|
+
sources.size + (value && !sources.include?(value) ? 1 : 0)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The form-encoded body (§2.2) and the URI query (§2.3) as Rack
|
|
112
|
+
# exposes them, for a request object that keeps the two apart.
|
|
113
|
+
def parameter_sources(request, parameter)
|
|
114
|
+
return [] unless request.respond_to?(:GET) && request.respond_to?(:POST)
|
|
115
|
+
|
|
116
|
+
query = request.GET
|
|
117
|
+
body = body_parameters(request)
|
|
118
|
+
return [] unless query.is_a?(Hash) && body.is_a?(Hash)
|
|
119
|
+
|
|
120
|
+
[query[parameter], body[parameter]].filter_map(&:presence)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# A body ActionDispatch cannot parse — malformed JSON under a JSON
|
|
124
|
+
# content type, say — carries no token. Reading it must not raise out
|
|
125
|
+
# of the extractors, which host apps call from places that run after
|
|
126
|
+
# their own ParseError handling (instrumentation, exception apps), the
|
|
127
|
+
# same way ActionDispatch's own #filtered_parameters treats that error.
|
|
128
|
+
def parameters(request)
|
|
129
|
+
request.parameters
|
|
130
|
+
rescue ActionDispatch::Http::Parameters::ParseError
|
|
131
|
+
{}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def body_parameters(request)
|
|
135
|
+
request.POST
|
|
136
|
+
rescue ActionDispatch::Http::Parameters::ParseError
|
|
137
|
+
{}
|
|
138
|
+
end
|
|
139
|
+
|
|
47
140
|
def token_from_basic_header(header, pattern)
|
|
48
141
|
encoded_header = token_from_header(header, pattern)
|
|
49
142
|
decode_basic_credentials_token(encoded_header)
|
|
@@ -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)
|
data/lib/doorkeeper/request.rb
CHANGED
|
@@ -3,18 +3,30 @@
|
|
|
3
3
|
module Doorkeeper
|
|
4
4
|
module Request
|
|
5
5
|
class << self
|
|
6
|
+
# Resolves the strategy that answers the given response_type at the
|
|
7
|
+
# authorization endpoint.
|
|
8
|
+
#
|
|
9
|
+
# A response_type no registered authorization flow handles is refused
|
|
10
|
+
# rather than turned into a class name: `constantize` would otherwise
|
|
11
|
+
# resolve any constant under Doorkeeper::Request, token endpoint
|
|
12
|
+
# strategies (`password`, `client_credentials`, `refresh_token`,
|
|
13
|
+
# `authorization_code`) included, none of which can answer an
|
|
14
|
+
# authorization request.
|
|
6
15
|
def authorization_strategy(response_type)
|
|
16
|
+
response_type = response_type.to_s
|
|
17
|
+
|
|
7
18
|
grant_flow = authorization_flows.detect do |flow|
|
|
8
19
|
flow.matches_response_type?(response_type)
|
|
9
20
|
end
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
raise Errors::InvalidTokenStrategy unless grant_flow
|
|
23
|
+
|
|
24
|
+
# [NOTE]: this will be removed in a newer versions of Doorkeeper.
|
|
25
|
+
# For retro-compatibility only: flows declared through the deprecated
|
|
26
|
+
# Config#calculate_authorization_response_types hook are built as
|
|
27
|
+
# GrantFlow::FallbackFlow and carry no strategy, so the class is looked
|
|
28
|
+
# up by name - but only for a response type the server does declare.
|
|
29
|
+
grant_flow.response_type_strategy || build_fallback_strategy_class(response_type)
|
|
18
30
|
end
|
|
19
31
|
|
|
20
32
|
def token_strategy(grant_type)
|
data/lib/doorkeeper/version.rb
CHANGED
|
@@ -283,6 +283,14 @@ Doorkeeper.configure do
|
|
|
283
283
|
# Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated
|
|
284
284
|
# for more information on customization
|
|
285
285
|
#
|
|
286
|
+
# NOTE: configuring a callable extractor opts the request out of the RFC 6749
|
|
287
|
+
# §2.3 checks that reject a request authenticating the client with more than
|
|
288
|
+
# one method, or presenting more than one client identity. A callable may
|
|
289
|
+
# legitimately overlap the built-in extractors, and the extractors after the
|
|
290
|
+
# matching one are never called, so what they would have returned is unknown.
|
|
291
|
+
# Prefer the symbol extractors unless you need something they cannot express;
|
|
292
|
+
# a callable has to reject such a request itself.
|
|
293
|
+
#
|
|
286
294
|
# client_credentials :from_basic, :from_params
|
|
287
295
|
|
|
288
296
|
# Change the way access token is authenticated from the request object.
|
|
@@ -308,10 +316,13 @@ Doorkeeper.configure do
|
|
|
308
316
|
# Specify what redirect URI's you want to block during Application creation.
|
|
309
317
|
# Any redirect URI is allowed by default.
|
|
310
318
|
#
|
|
311
|
-
#
|
|
319
|
+
# Redirect URIs with a script scheme (`javascript`, `vbscript`, `data`) are
|
|
320
|
+
# always rejected, regardless of this option.
|
|
321
|
+
#
|
|
322
|
+
# You can use this option in order to forbid URI's of a particular host
|
|
312
323
|
# for example.
|
|
313
324
|
#
|
|
314
|
-
# forbid_redirect_uri { |uri| uri.
|
|
325
|
+
# forbid_redirect_uri { |uri| uri.host == 'example.com' }
|
|
315
326
|
|
|
316
327
|
# Allows to set blank redirect URIs for Applications in case Doorkeeper configured
|
|
317
328
|
# to use URI-less OAuth grant flows like Client Credentials or Resource Owner
|
metadata
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: doorkeeper
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.9.
|
|
4
|
+
version: 5.9.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Felipe Elias Philipp
|
|
8
8
|
- Tute Costa
|
|
9
9
|
- Jon Moss
|
|
10
10
|
- Nikita Bulai
|
|
11
|
+
autorequire:
|
|
11
12
|
bindir: bin
|
|
12
13
|
cert_chain: []
|
|
13
|
-
date:
|
|
14
|
+
date: 2026-09-24 00:00:00.000000000 Z
|
|
14
15
|
dependencies:
|
|
15
16
|
- !ruby/object:Gem::Dependency
|
|
16
17
|
name: railties
|
|
@@ -329,6 +330,7 @@ metadata:
|
|
|
329
330
|
bug_tracker_uri: https://github.com/doorkeeper-gem/doorkeeper/issues
|
|
330
331
|
documentation_uri: https://doorkeeper.gitbook.io/guides/
|
|
331
332
|
funding_uri: https://opencollective.com/doorkeeper-gem
|
|
333
|
+
post_install_message:
|
|
332
334
|
rdoc_options: []
|
|
333
335
|
require_paths:
|
|
334
336
|
- lib
|
|
@@ -343,7 +345,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
343
345
|
- !ruby/object:Gem::Version
|
|
344
346
|
version: '0'
|
|
345
347
|
requirements: []
|
|
346
|
-
rubygems_version:
|
|
348
|
+
rubygems_version: 3.5.16
|
|
349
|
+
signing_key:
|
|
347
350
|
specification_version: 4
|
|
348
351
|
summary: OAuth 2 provider for Rails and Grape
|
|
349
352
|
test_files: []
|