doorkeeper 5.0.2 → 5.1.0.rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.travis.yml +17 -4
- data/Appraisals +29 -3
- data/Dangerfile +5 -2
- data/Gemfile +14 -4
- data/NEWS.md +64 -19
- data/README.md +68 -487
- data/app/controllers/doorkeeper/token_info_controller.rb +1 -1
- data/app/controllers/doorkeeper/tokens_controller.rb +7 -7
- data/app/views/doorkeeper/applications/show.html.erb +1 -1
- data/app/views/layouts/doorkeeper/admin.html.erb +5 -3
- data/bin/console +15 -0
- data/doorkeeper.gemspec +3 -2
- data/gemfiles/rails_4_2.gemfile +8 -4
- data/gemfiles/rails_5_0.gemfile +9 -5
- data/gemfiles/rails_5_1.gemfile +9 -5
- data/gemfiles/rails_5_2.gemfile +9 -5
- data/gemfiles/rails_6_0.gemfile +16 -0
- data/gemfiles/rails_master.gemfile +8 -9
- data/lib/doorkeeper/config.rb +164 -11
- data/lib/doorkeeper/helpers/controller.rb +3 -2
- data/lib/doorkeeper/models/access_grant_mixin.rb +16 -1
- data/lib/doorkeeper/models/access_token_mixin.rb +54 -10
- data/lib/doorkeeper/models/application_mixin.rb +42 -1
- data/lib/doorkeeper/models/concerns/expirable.rb +3 -2
- data/lib/doorkeeper/models/concerns/reusable.rb +19 -0
- data/lib/doorkeeper/models/concerns/scopes.rb +5 -1
- data/lib/doorkeeper/models/concerns/secret_storable.rb +106 -0
- data/lib/doorkeeper/oauth/authorization/code.rb +1 -1
- data/lib/doorkeeper/oauth/authorization/token.rb +4 -2
- data/lib/doorkeeper/oauth/authorization_code_request.rb +1 -1
- data/lib/doorkeeper/oauth/client.rb +1 -1
- data/lib/doorkeeper/oauth/client_credentials/validation.rb +4 -3
- data/lib/doorkeeper/oauth/code_response.rb +2 -2
- data/lib/doorkeeper/oauth/error_response.rb +5 -1
- data/lib/doorkeeper/oauth/helpers/scope_checker.rb +23 -8
- data/lib/doorkeeper/oauth/helpers/unique_token.rb +12 -1
- data/lib/doorkeeper/oauth/helpers/uri_checker.rb +32 -0
- data/lib/doorkeeper/oauth/invalid_token_response.rb +4 -0
- data/lib/doorkeeper/oauth/password_access_token_request.rb +7 -2
- data/lib/doorkeeper/oauth/pre_authorization.rb +8 -3
- data/lib/doorkeeper/oauth/refresh_token_request.rb +4 -1
- data/lib/doorkeeper/oauth/token_introspection.rb +72 -6
- data/lib/doorkeeper/oauth/token_response.rb +2 -2
- data/lib/doorkeeper/orm/active_record/access_grant.rb +23 -2
- data/lib/doorkeeper/orm/active_record/application.rb +20 -2
- data/lib/doorkeeper/secret_storing/base.rb +63 -0
- data/lib/doorkeeper/secret_storing/bcrypt.rb +59 -0
- data/lib/doorkeeper/secret_storing/plain.rb +33 -0
- data/lib/doorkeeper/secret_storing/sha256_hash.rb +25 -0
- data/lib/doorkeeper/version.rb +3 -3
- data/lib/doorkeeper.rb +7 -0
- data/lib/generators/doorkeeper/templates/initializer.rb +90 -8
- data/spec/controllers/application_metal_controller_spec.rb +18 -4
- data/spec/controllers/authorizations_controller_spec.rb +3 -3
- data/spec/controllers/token_info_controller_spec.rb +1 -1
- data/spec/controllers/tokens_controller_spec.rb +85 -41
- data/spec/dummy/app/controllers/application_controller.rb +1 -1
- data/spec/dummy/config/application.rb +12 -1
- data/spec/factories.rb +3 -3
- data/spec/lib/config_spec.rb +168 -0
- data/spec/lib/models/expirable_spec.rb +12 -0
- data/spec/lib/models/reusable_spec.rb +40 -0
- data/spec/lib/models/scopes_spec.rb +13 -1
- data/spec/lib/models/secret_storable_spec.rb +113 -0
- data/spec/lib/oauth/authorization_code_request_spec.rb +18 -1
- data/spec/lib/oauth/base_request_spec.rb +7 -7
- data/spec/lib/oauth/client_credentials/creator_spec.rb +51 -7
- data/spec/lib/oauth/client_credentials/validation_spec.rb +3 -0
- data/spec/lib/oauth/error_response_spec.rb +7 -1
- data/spec/lib/oauth/helpers/scope_checker_spec.rb +52 -17
- data/spec/lib/oauth/helpers/uri_checker_spec.rb +20 -2
- data/spec/lib/oauth/password_access_token_request_spec.rb +43 -12
- data/spec/lib/oauth/pre_authorization_spec.rb +24 -0
- data/spec/lib/oauth/token_request_spec.rb +16 -1
- data/spec/lib/oauth/token_response_spec.rb +13 -13
- data/spec/lib/oauth/token_spec.rb +14 -0
- data/spec/lib/secret_storing/base_spec.rb +60 -0
- data/spec/lib/secret_storing/bcrypt_spec.rb +49 -0
- data/spec/lib/secret_storing/plain_spec.rb +44 -0
- data/spec/lib/secret_storing/sha256_hash_spec.rb +48 -0
- data/spec/models/doorkeeper/access_grant_spec.rb +61 -0
- data/spec/models/doorkeeper/access_token_spec.rb +123 -0
- data/spec/models/doorkeeper/application_spec.rb +56 -0
- data/spec/requests/flows/authorization_code_spec.rb +41 -1
- data/spec/requests/flows/client_credentials_spec.rb +2 -2
- data/spec/requests/flows/implicit_grant_spec.rb +1 -1
- data/spec/requests/flows/password_spec.rb +7 -5
- data/spec/requests/flows/revoke_token_spec.rb +14 -30
- data/spec/routing/custom_controller_routes_spec.rb +4 -0
- data/spec/spec_helper.rb +2 -1
- data/spec/support/ruby_2_6_rails_4_2_patch.rb +14 -0
- data/spec/support/shared/hashing_shared_context.rb +36 -0
- metadata +59 -22
|
@@ -6,8 +6,10 @@ module Doorkeeper
|
|
|
6
6
|
|
|
7
7
|
include OAuth::Helpers
|
|
8
8
|
include Models::Orderable
|
|
9
|
+
include Models::SecretStorable
|
|
9
10
|
include Models::Scopes
|
|
10
11
|
|
|
12
|
+
# :nodoc
|
|
11
13
|
module ClassMethods
|
|
12
14
|
# Returns an instance of the Doorkeeper::Application with
|
|
13
15
|
# specific UID and secret.
|
|
@@ -25,7 +27,7 @@ module Doorkeeper
|
|
|
25
27
|
app = by_uid(uid)
|
|
26
28
|
return unless app
|
|
27
29
|
return app if secret.blank? && !app.confidential?
|
|
28
|
-
return unless app.secret
|
|
30
|
+
return unless app.secret_matches?(secret)
|
|
29
31
|
app
|
|
30
32
|
end
|
|
31
33
|
|
|
@@ -39,6 +41,20 @@ module Doorkeeper
|
|
|
39
41
|
def by_uid(uid)
|
|
40
42
|
find_by(uid: uid.to_s)
|
|
41
43
|
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Determines the secret storing transformer
|
|
47
|
+
# Unless configured otherwise, uses the plain secret strategy
|
|
48
|
+
def secret_strategy
|
|
49
|
+
::Doorkeeper.configuration.application_secret_strategy
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# Determine the fallback storing strategy
|
|
54
|
+
# Unless configured, there will be no fallback
|
|
55
|
+
def fallback_secret_strategy
|
|
56
|
+
::Doorkeeper.configuration.application_secret_fallback_strategy
|
|
57
|
+
end
|
|
42
58
|
end
|
|
43
59
|
|
|
44
60
|
# Set an application's valid redirect URIs.
|
|
@@ -49,5 +65,30 @@ module Doorkeeper
|
|
|
49
65
|
def redirect_uri=(uris)
|
|
50
66
|
super(uris.is_a?(Array) ? uris.join("\n") : uris)
|
|
51
67
|
end
|
|
68
|
+
|
|
69
|
+
# Check whether the given plain text secret matches our stored secret
|
|
70
|
+
#
|
|
71
|
+
# @param input [#to_s] Plain secret provided by user
|
|
72
|
+
# (any object that responds to `#to_s`)
|
|
73
|
+
#
|
|
74
|
+
# @return [true] Whether the given secret matches the stored secret
|
|
75
|
+
# of this application.
|
|
76
|
+
#
|
|
77
|
+
def secret_matches?(input)
|
|
78
|
+
# return false if either is nil, since secure_compare depends on strings
|
|
79
|
+
# but Application secrets MAY be nil depending on confidentiality.
|
|
80
|
+
return false if input.nil? || secret.nil?
|
|
81
|
+
|
|
82
|
+
# When matching the secret by comparer function, all is well.
|
|
83
|
+
return true if secret_strategy.secret_matches?(input, secret)
|
|
84
|
+
|
|
85
|
+
# When fallback lookup is enabled, ensure applications
|
|
86
|
+
# with plain secrets can still be found
|
|
87
|
+
if fallback_secret_strategy
|
|
88
|
+
fallback_secret_strategy.secret_matches?(input, secret)
|
|
89
|
+
else
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
end
|
|
52
93
|
end
|
|
53
94
|
end
|
|
@@ -24,10 +24,11 @@ module Doorkeeper
|
|
|
24
24
|
|
|
25
25
|
# Expiration time (date time of creation + TTL).
|
|
26
26
|
#
|
|
27
|
-
# @return [Time] expiration time in UTC
|
|
27
|
+
# @return [Time, nil] expiration time in UTC
|
|
28
|
+
# or nil if the object never expires.
|
|
28
29
|
#
|
|
29
30
|
def expires_at
|
|
30
|
-
created_at + expires_in.seconds
|
|
31
|
+
expires_in && created_at + expires_in.seconds
|
|
31
32
|
end
|
|
32
33
|
end
|
|
33
34
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
module Models
|
|
5
|
+
module Reusable
|
|
6
|
+
# Indicates whether the object is reusable (i.e. It is not expired and
|
|
7
|
+
# has not crossed reuse_limit).
|
|
8
|
+
#
|
|
9
|
+
# @return [Boolean] true if can be reused and false in other case
|
|
10
|
+
def reusable?
|
|
11
|
+
return false if expired?
|
|
12
|
+
return true unless expires_in
|
|
13
|
+
|
|
14
|
+
threshold_limit = 100 - Doorkeeper.configuration.token_reuse_limit
|
|
15
|
+
expires_in_seconds >= threshold_limit * expires_in / 100
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
module Models
|
|
5
|
+
##
|
|
6
|
+
# Storable finder to provide lookups for input plaintext values which are
|
|
7
|
+
# mapped to their stored versions (e.g., hashing, encryption) before lookup.
|
|
8
|
+
module SecretStorable
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
delegate :secret_strategy,
|
|
12
|
+
:fallback_secret_strategy,
|
|
13
|
+
to: :class
|
|
14
|
+
|
|
15
|
+
# :nodoc
|
|
16
|
+
module ClassMethods
|
|
17
|
+
# Compare the given plaintext with the secret
|
|
18
|
+
#
|
|
19
|
+
# @param input [String]
|
|
20
|
+
# The plain input to compare.
|
|
21
|
+
#
|
|
22
|
+
# @param secret [String]
|
|
23
|
+
# The secret value to compare with.
|
|
24
|
+
#
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
# Whether input matches secret as per the secret strategy
|
|
27
|
+
#
|
|
28
|
+
def secret_matches?(input, secret)
|
|
29
|
+
secret_strategy.secret_matches?(input, secret)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns an instance of the Doorkeeper::AccessToken with
|
|
33
|
+
# specific token value.
|
|
34
|
+
#
|
|
35
|
+
# @param attr [Symbol]
|
|
36
|
+
# The token attribute we're looking with.
|
|
37
|
+
#
|
|
38
|
+
# @param token [#to_s]
|
|
39
|
+
# token value (any object that responds to `#to_s`)
|
|
40
|
+
#
|
|
41
|
+
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
|
|
42
|
+
# if there is no record with such token
|
|
43
|
+
#
|
|
44
|
+
def find_by_plaintext_token(attr, token)
|
|
45
|
+
token = token.to_s
|
|
46
|
+
|
|
47
|
+
find_by(attr => secret_strategy.transform_secret(token)) ||
|
|
48
|
+
find_by_fallback_token(attr, token)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Allow looking up previously plain tokens as a fallback
|
|
52
|
+
# IFF a fallback strategy has been defined
|
|
53
|
+
#
|
|
54
|
+
# @param attr [Symbol]
|
|
55
|
+
# The token attribute we're looking with.
|
|
56
|
+
#
|
|
57
|
+
# @param plain_secret [#to_s]
|
|
58
|
+
# plain secret value (any object that responds to `#to_s`)
|
|
59
|
+
#
|
|
60
|
+
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
|
|
61
|
+
# if there is no record with such token
|
|
62
|
+
#
|
|
63
|
+
def find_by_fallback_token(attr, plain_secret)
|
|
64
|
+
return nil unless fallback_secret_strategy
|
|
65
|
+
|
|
66
|
+
# Use the previous strategy to look up
|
|
67
|
+
stored_token = fallback_secret_strategy.transform_secret(plain_secret)
|
|
68
|
+
find_by(attr => stored_token).tap do |resource|
|
|
69
|
+
upgrade_fallback_value resource, attr, plain_secret
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Allow implementations in ORMs to replace a plain
|
|
74
|
+
# value falling back to to avoid it remaining as plain text.
|
|
75
|
+
#
|
|
76
|
+
# @param instance
|
|
77
|
+
# An instance of this model with a plain value token.
|
|
78
|
+
#
|
|
79
|
+
# @param attr
|
|
80
|
+
# The secret attribute name to upgrade.
|
|
81
|
+
#
|
|
82
|
+
# @param plain_secret
|
|
83
|
+
# The plain secret to upgrade.
|
|
84
|
+
#
|
|
85
|
+
def upgrade_fallback_value(instance, attr, plain_secret)
|
|
86
|
+
upgraded = secret_strategy.store_secret(instance, attr, plain_secret)
|
|
87
|
+
instance.update(attr => upgraded)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Determines the secret storing transformer
|
|
92
|
+
# Unless configured otherwise, uses the plain secret strategy
|
|
93
|
+
def secret_strategy
|
|
94
|
+
::Doorkeeper::SecretStoring::Plain
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
##
|
|
98
|
+
# Determine the fallback storing strategy
|
|
99
|
+
# Unless configured, there will be no fallback
|
|
100
|
+
def fallback_secret_strategy
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -8,7 +8,9 @@ module Doorkeeper
|
|
|
8
8
|
|
|
9
9
|
class << self
|
|
10
10
|
def build_context(pre_auth_or_oauth_client, grant_type, scopes)
|
|
11
|
-
oauth_client = if pre_auth_or_oauth_client.respond_to?(:
|
|
11
|
+
oauth_client = if pre_auth_or_oauth_client.respond_to?(:application)
|
|
12
|
+
pre_auth_or_oauth_client.application
|
|
13
|
+
elsif pre_auth_or_oauth_client.respond_to?(:client)
|
|
12
14
|
pre_auth_or_oauth_client.client
|
|
13
15
|
else
|
|
14
16
|
pre_auth_or_oauth_client
|
|
@@ -62,7 +64,7 @@ module Doorkeeper
|
|
|
62
64
|
{
|
|
63
65
|
controller: controller,
|
|
64
66
|
action: :show,
|
|
65
|
-
access_token: token.
|
|
67
|
+
access_token: token.plaintext_token
|
|
66
68
|
}
|
|
67
69
|
end
|
|
68
70
|
|
|
@@ -18,7 +18,7 @@ module Doorkeeper
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def self.authenticate(credentials, method = Application.method(:by_uid_and_secret))
|
|
21
|
-
return
|
|
21
|
+
return if credentials.blank?
|
|
22
22
|
|
|
23
23
|
if (application = method.call(credentials.uid, credentials.secret))
|
|
24
24
|
new(application)
|
|
@@ -34,9 +34,10 @@ module Doorkeeper
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
ScopeChecker.valid?(
|
|
37
|
-
@request.scopes.to_s,
|
|
38
|
-
@server.scopes,
|
|
39
|
-
application_scopes
|
|
37
|
+
scope_str: @request.scopes.to_s,
|
|
38
|
+
server_scopes: @server.scopes,
|
|
39
|
+
app_scopes: application_scopes,
|
|
40
|
+
grant_type: Doorkeeper::OAuth::CLIENT_CREDENTIALS
|
|
40
41
|
)
|
|
41
42
|
end
|
|
42
43
|
end
|
|
@@ -23,7 +23,7 @@ module Doorkeeper
|
|
|
23
23
|
elsif response_on_fragment
|
|
24
24
|
Authorization::URIBuilder.uri_with_fragment(
|
|
25
25
|
pre_auth.redirect_uri,
|
|
26
|
-
access_token: auth.token.
|
|
26
|
+
access_token: auth.token.plaintext_token,
|
|
27
27
|
token_type: auth.token.token_type,
|
|
28
28
|
expires_in: auth.token.expires_in_seconds,
|
|
29
29
|
state: pre_auth.state
|
|
@@ -31,7 +31,7 @@ module Doorkeeper
|
|
|
31
31
|
else
|
|
32
32
|
Authorization::URIBuilder.uri_with_query(
|
|
33
33
|
pre_auth.redirect_uri,
|
|
34
|
-
code: auth.token.
|
|
34
|
+
code: auth.token.plaintext_token,
|
|
35
35
|
state: pre_auth.state
|
|
36
36
|
)
|
|
37
37
|
end
|
|
@@ -7,31 +7,46 @@ module Doorkeeper
|
|
|
7
7
|
class Validator
|
|
8
8
|
attr_reader :parsed_scopes, :scope_str
|
|
9
9
|
|
|
10
|
-
def initialize(scope_str, server_scopes,
|
|
10
|
+
def initialize(scope_str, server_scopes, app_scopes, grant_type)
|
|
11
11
|
@parsed_scopes = OAuth::Scopes.from_string(scope_str)
|
|
12
12
|
@scope_str = scope_str
|
|
13
|
-
@valid_scopes = valid_scopes(server_scopes,
|
|
13
|
+
@valid_scopes = valid_scopes(server_scopes, app_scopes)
|
|
14
|
+
|
|
15
|
+
if grant_type
|
|
16
|
+
@scopes_by_grant_type = Doorkeeper.configuration.scopes_by_grant_type[grant_type.to_sym]
|
|
17
|
+
end
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
def valid?
|
|
17
21
|
scope_str.present? &&
|
|
18
22
|
scope_str !~ /[\n\r\t]/ &&
|
|
19
|
-
@valid_scopes.has_scopes?(parsed_scopes)
|
|
23
|
+
@valid_scopes.has_scopes?(parsed_scopes) &&
|
|
24
|
+
permitted_to_grant_type?
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
private
|
|
23
28
|
|
|
24
|
-
def valid_scopes(server_scopes,
|
|
25
|
-
if
|
|
26
|
-
|
|
29
|
+
def valid_scopes(server_scopes, app_scopes)
|
|
30
|
+
if app_scopes.present?
|
|
31
|
+
app_scopes
|
|
27
32
|
else
|
|
28
33
|
server_scopes
|
|
29
34
|
end
|
|
30
35
|
end
|
|
36
|
+
|
|
37
|
+
def permitted_to_grant_type?
|
|
38
|
+
return true unless @scopes_by_grant_type
|
|
39
|
+
|
|
40
|
+
OAuth::Scopes.from_array(@scopes_by_grant_type)
|
|
41
|
+
.has_scopes?(parsed_scopes)
|
|
42
|
+
end
|
|
31
43
|
end
|
|
32
44
|
|
|
33
|
-
def self.valid?(scope_str
|
|
34
|
-
Validator.new(scope_str,
|
|
45
|
+
def self.valid?(scope_str:, server_scopes:, app_scopes: nil, grant_type: nil)
|
|
46
|
+
Validator.new(scope_str,
|
|
47
|
+
server_scopes,
|
|
48
|
+
app_scopes,
|
|
49
|
+
grant_type).valid?
|
|
35
50
|
end
|
|
36
51
|
end
|
|
37
52
|
end
|
|
@@ -5,10 +5,21 @@ module Doorkeeper
|
|
|
5
5
|
module Helpers
|
|
6
6
|
module UniqueToken
|
|
7
7
|
def self.generate(options = {})
|
|
8
|
-
|
|
8
|
+
# Access Token value must be 1*VSCHAR or 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
|
|
9
|
+
#
|
|
10
|
+
# @see https://tools.ietf.org/html/rfc6749#appendix-A.12
|
|
11
|
+
# @see https://tools.ietf.org/html/rfc6750#section-2.1
|
|
12
|
+
#
|
|
13
|
+
generator_method = options.delete(:generator) || SecureRandom.method(self.generator_method)
|
|
9
14
|
token_size = options.delete(:size) || 32
|
|
10
15
|
generator_method.call(token_size)
|
|
11
16
|
end
|
|
17
|
+
|
|
18
|
+
# Generator method for default generator class (SecureRandom)
|
|
19
|
+
#
|
|
20
|
+
def self.generator_method
|
|
21
|
+
Doorkeeper.configuration.default_generator_method
|
|
22
|
+
end
|
|
12
23
|
end
|
|
13
24
|
end
|
|
14
25
|
end
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require 'ipaddr'
|
|
2
3
|
|
|
3
4
|
module Doorkeeper
|
|
5
|
+
module IPAddrLoopback
|
|
6
|
+
def loopback?
|
|
7
|
+
case @family
|
|
8
|
+
when Socket::AF_INET
|
|
9
|
+
@addr & 0xff000000 == 0x7f000000
|
|
10
|
+
when Socket::AF_INET6
|
|
11
|
+
@addr == 1
|
|
12
|
+
else
|
|
13
|
+
raise AddressFamilyError, "unsupported address family"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# For backward compatibility with old rubies
|
|
19
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.5.0")
|
|
20
|
+
IPAddr.send(:include, Doorkeeper::IPAddrLoopback)
|
|
21
|
+
end
|
|
22
|
+
|
|
4
23
|
module OAuth
|
|
5
24
|
module Helpers
|
|
6
25
|
module URIChecker
|
|
@@ -23,10 +42,23 @@ module Doorkeeper
|
|
|
23
42
|
client_url.query = nil
|
|
24
43
|
end
|
|
25
44
|
|
|
45
|
+
# RFC8252, Paragraph 7.3
|
|
46
|
+
# @see https://tools.ietf.org/html/rfc8252#section-7.3
|
|
47
|
+
if loopback_uri?(url) && loopback_uri?(client_url)
|
|
48
|
+
url.port = nil
|
|
49
|
+
client_url.port = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
26
52
|
url.query = nil
|
|
27
53
|
url == client_url
|
|
28
54
|
end
|
|
29
55
|
|
|
56
|
+
def self.loopback_uri?(uri)
|
|
57
|
+
IPAddr.new(uri.host).loopback?
|
|
58
|
+
rescue IPAddr::Error
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
30
62
|
def self.valid_for_authorization?(url, client_url)
|
|
31
63
|
valid?(url) && client_url.split.any? { |other_url| matches?(url, other_url) }
|
|
32
64
|
end
|
|
@@ -32,7 +32,12 @@ module Doorkeeper
|
|
|
32
32
|
client_scopes = client.try(:scopes)
|
|
33
33
|
return true if scopes.blank?
|
|
34
34
|
|
|
35
|
-
ScopeChecker.valid?(
|
|
35
|
+
ScopeChecker.valid?(
|
|
36
|
+
scope_str: scopes.to_s,
|
|
37
|
+
server_scopes: server.scopes,
|
|
38
|
+
app_scopes: client_scopes,
|
|
39
|
+
grant_type: grant_type
|
|
40
|
+
)
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
def validate_resource_owner
|
|
@@ -40,7 +45,7 @@ module Doorkeeper
|
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
def validate_client
|
|
43
|
-
!parameters[:client_id] ||
|
|
48
|
+
!parameters[:client_id] || client.present?
|
|
44
49
|
end
|
|
45
50
|
end
|
|
46
51
|
end
|
|
@@ -77,12 +77,17 @@ module Doorkeeper
|
|
|
77
77
|
return true if scope.blank?
|
|
78
78
|
|
|
79
79
|
Helpers::ScopeChecker.valid?(
|
|
80
|
-
scope,
|
|
81
|
-
server.scopes,
|
|
82
|
-
client.application.scopes
|
|
80
|
+
scope_str: scope,
|
|
81
|
+
server_scopes: server.scopes,
|
|
82
|
+
app_scopes: client.application.scopes,
|
|
83
|
+
grant_type: grant_type
|
|
83
84
|
)
|
|
84
85
|
end
|
|
85
86
|
|
|
87
|
+
def grant_type
|
|
88
|
+
response_type == 'code' ? AUTHORIZATION_CODE : IMPLICIT
|
|
89
|
+
end
|
|
90
|
+
|
|
86
91
|
def validate_redirect_uri
|
|
87
92
|
return false if redirect_uri.blank?
|
|
88
93
|
|
|
@@ -99,7 +99,10 @@ module Doorkeeper
|
|
|
99
99
|
|
|
100
100
|
def validate_scope
|
|
101
101
|
if @original_scopes.present?
|
|
102
|
-
ScopeChecker.valid?(
|
|
102
|
+
ScopeChecker.valid?(
|
|
103
|
+
scope_str: @original_scopes,
|
|
104
|
+
server_scopes: refresh_token.scopes
|
|
105
|
+
)
|
|
103
106
|
else
|
|
104
107
|
true
|
|
105
108
|
end
|
|
@@ -20,6 +20,16 @@ module Doorkeeper
|
|
|
20
20
|
@error.blank?
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
def error_response
|
|
24
|
+
return if @error.blank?
|
|
25
|
+
|
|
26
|
+
if @error == :invalid_token
|
|
27
|
+
OAuth::InvalidTokenResponse.from_access_token(authorized_token)
|
|
28
|
+
else
|
|
29
|
+
OAuth::ErrorResponse.new(name: @error)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
23
33
|
def to_json
|
|
24
34
|
active? ? success_response : failure_response
|
|
25
35
|
end
|
|
@@ -37,13 +47,29 @@ module Doorkeeper
|
|
|
37
47
|
#
|
|
38
48
|
# @see https://www.oauth.com/oauth2-servers/token-introspection-endpoint/
|
|
39
49
|
#
|
|
50
|
+
# To prevent token scanning attacks, the endpoint MUST also require
|
|
51
|
+
# some form of authorization to access this endpoint, such as client
|
|
52
|
+
# authentication as described in OAuth 2.0 [RFC6749] or a separate
|
|
53
|
+
# OAuth 2.0 access token such as the bearer token described in OAuth
|
|
54
|
+
# 2.0 Bearer Token Usage [RFC6750].
|
|
55
|
+
#
|
|
40
56
|
def authorize!
|
|
41
57
|
# Requested client authorization
|
|
42
58
|
if server.credentials
|
|
43
59
|
@error = :invalid_client unless authorized_client
|
|
44
|
-
|
|
60
|
+
elsif authorized_token
|
|
45
61
|
# Requested bearer token authorization
|
|
46
|
-
|
|
62
|
+
#
|
|
63
|
+
# If the protected resource uses an OAuth 2.0 bearer token to authorize
|
|
64
|
+
# its call to the introspection endpoint and the token used for
|
|
65
|
+
# authorization does not contain sufficient privileges or is otherwise
|
|
66
|
+
# invalid for this request, the authorization server responds with an
|
|
67
|
+
# HTTP 401 code as described in Section 3 of OAuth 2.0 Bearer Token
|
|
68
|
+
# Usage [RFC6750].
|
|
69
|
+
#
|
|
70
|
+
@error = :invalid_token if authorized_token_matches_introspected? || !authorized_token.accessible?
|
|
71
|
+
else
|
|
72
|
+
@error = :invalid_request
|
|
47
73
|
end
|
|
48
74
|
end
|
|
49
75
|
|
|
@@ -60,14 +86,14 @@ module Doorkeeper
|
|
|
60
86
|
|
|
61
87
|
# 2.2. Introspection Response
|
|
62
88
|
def success_response
|
|
63
|
-
|
|
89
|
+
customize_response(
|
|
64
90
|
active: true,
|
|
65
91
|
scope: @token.scopes_string,
|
|
66
92
|
client_id: @token.try(:application).try(:uid),
|
|
67
93
|
token_type: @token.token_type,
|
|
68
94
|
exp: @token.expires_at.to_i,
|
|
69
95
|
iat: @token.created_at.to_i
|
|
70
|
-
|
|
96
|
+
)
|
|
71
97
|
end
|
|
72
98
|
|
|
73
99
|
# If the introspection call is properly authorized but the token is not
|
|
@@ -103,6 +129,25 @@ module Doorkeeper
|
|
|
103
129
|
# * The token expired
|
|
104
130
|
# * The token was issued to a different client than is making this request
|
|
105
131
|
#
|
|
132
|
+
# Since resource servers using token introspection rely on the
|
|
133
|
+
# authorization server to determine the state of a token, the
|
|
134
|
+
# authorization server MUST perform all applicable checks against a
|
|
135
|
+
# token's state. For instance, these tests include the following:
|
|
136
|
+
#
|
|
137
|
+
# o If the token can expire, the authorization server MUST determine
|
|
138
|
+
# whether or not the token has expired.
|
|
139
|
+
# o If the token can be issued before it is able to be used, the
|
|
140
|
+
# authorization server MUST determine whether or not a token's valid
|
|
141
|
+
# period has started yet.
|
|
142
|
+
# o If the token can be revoked after it was issued, the authorization
|
|
143
|
+
# server MUST determine whether or not such a revocation has taken
|
|
144
|
+
# place.
|
|
145
|
+
# o If the token has been signed, the authorization server MUST
|
|
146
|
+
# validate the signature.
|
|
147
|
+
# o If the token can be used only at certain resource servers, the
|
|
148
|
+
# authorization server MUST determine whether or not the token can
|
|
149
|
+
# be used at the resource server making the introspection call.
|
|
150
|
+
#
|
|
106
151
|
def active?
|
|
107
152
|
if authorized_client
|
|
108
153
|
valid_token? && authorized_for_client?
|
|
@@ -113,18 +158,39 @@ module Doorkeeper
|
|
|
113
158
|
|
|
114
159
|
# Token can be valid only if it is not expired or revoked.
|
|
115
160
|
def valid_token?
|
|
116
|
-
@token
|
|
161
|
+
@token && @token.accessible?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# RFC7662 Section 2.1
|
|
165
|
+
def authorized_token_matches_introspected?
|
|
166
|
+
authorized_token.token == @token.token
|
|
117
167
|
end
|
|
118
168
|
|
|
119
169
|
# If token doesn't belong to some client, then it is public.
|
|
120
170
|
# Otherwise in it required for token to be connected to the same client.
|
|
121
171
|
def authorized_for_client?
|
|
122
|
-
if @token.application
|
|
172
|
+
if @token.application
|
|
123
173
|
@token.application == authorized_client.application
|
|
124
174
|
else
|
|
125
175
|
true
|
|
126
176
|
end
|
|
127
177
|
end
|
|
178
|
+
|
|
179
|
+
# Allows to customize introspection response.
|
|
180
|
+
# Provides context (controller) and token for generating developer-specific
|
|
181
|
+
# response.
|
|
182
|
+
#
|
|
183
|
+
# @see https://tools.ietf.org/html/rfc7662#section-2.2
|
|
184
|
+
#
|
|
185
|
+
def customize_response(response)
|
|
186
|
+
customized_response = Doorkeeper.configuration.custom_introspection_response.call(
|
|
187
|
+
token,
|
|
188
|
+
server.context
|
|
189
|
+
)
|
|
190
|
+
return response if customized_response.blank?
|
|
191
|
+
|
|
192
|
+
response.merge(customized_response)
|
|
193
|
+
end
|
|
128
194
|
end
|
|
129
195
|
end
|
|
130
196
|
end
|
|
@@ -11,10 +11,10 @@ module Doorkeeper
|
|
|
11
11
|
|
|
12
12
|
def body
|
|
13
13
|
{
|
|
14
|
-
'access_token' => token.
|
|
14
|
+
'access_token' => token.plaintext_token,
|
|
15
15
|
'token_type' => token.token_type,
|
|
16
16
|
'expires_in' => token.expires_in_seconds,
|
|
17
|
-
'refresh_token' => token.
|
|
17
|
+
'refresh_token' => token.plaintext_refresh_token,
|
|
18
18
|
'scope' => token.scopes_string,
|
|
19
19
|
'created_at' => token.created_at.to_i
|
|
20
20
|
}.reject { |_, value| value.blank? }
|