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.
Files changed (94) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +17 -4
  3. data/Appraisals +29 -3
  4. data/Dangerfile +5 -2
  5. data/Gemfile +14 -4
  6. data/NEWS.md +64 -19
  7. data/README.md +68 -487
  8. data/app/controllers/doorkeeper/token_info_controller.rb +1 -1
  9. data/app/controllers/doorkeeper/tokens_controller.rb +7 -7
  10. data/app/views/doorkeeper/applications/show.html.erb +1 -1
  11. data/app/views/layouts/doorkeeper/admin.html.erb +5 -3
  12. data/bin/console +15 -0
  13. data/doorkeeper.gemspec +3 -2
  14. data/gemfiles/rails_4_2.gemfile +8 -4
  15. data/gemfiles/rails_5_0.gemfile +9 -5
  16. data/gemfiles/rails_5_1.gemfile +9 -5
  17. data/gemfiles/rails_5_2.gemfile +9 -5
  18. data/gemfiles/rails_6_0.gemfile +16 -0
  19. data/gemfiles/rails_master.gemfile +8 -9
  20. data/lib/doorkeeper/config.rb +164 -11
  21. data/lib/doorkeeper/helpers/controller.rb +3 -2
  22. data/lib/doorkeeper/models/access_grant_mixin.rb +16 -1
  23. data/lib/doorkeeper/models/access_token_mixin.rb +54 -10
  24. data/lib/doorkeeper/models/application_mixin.rb +42 -1
  25. data/lib/doorkeeper/models/concerns/expirable.rb +3 -2
  26. data/lib/doorkeeper/models/concerns/reusable.rb +19 -0
  27. data/lib/doorkeeper/models/concerns/scopes.rb +5 -1
  28. data/lib/doorkeeper/models/concerns/secret_storable.rb +106 -0
  29. data/lib/doorkeeper/oauth/authorization/code.rb +1 -1
  30. data/lib/doorkeeper/oauth/authorization/token.rb +4 -2
  31. data/lib/doorkeeper/oauth/authorization_code_request.rb +1 -1
  32. data/lib/doorkeeper/oauth/client.rb +1 -1
  33. data/lib/doorkeeper/oauth/client_credentials/validation.rb +4 -3
  34. data/lib/doorkeeper/oauth/code_response.rb +2 -2
  35. data/lib/doorkeeper/oauth/error_response.rb +5 -1
  36. data/lib/doorkeeper/oauth/helpers/scope_checker.rb +23 -8
  37. data/lib/doorkeeper/oauth/helpers/unique_token.rb +12 -1
  38. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +32 -0
  39. data/lib/doorkeeper/oauth/invalid_token_response.rb +4 -0
  40. data/lib/doorkeeper/oauth/password_access_token_request.rb +7 -2
  41. data/lib/doorkeeper/oauth/pre_authorization.rb +8 -3
  42. data/lib/doorkeeper/oauth/refresh_token_request.rb +4 -1
  43. data/lib/doorkeeper/oauth/token_introspection.rb +72 -6
  44. data/lib/doorkeeper/oauth/token_response.rb +2 -2
  45. data/lib/doorkeeper/orm/active_record/access_grant.rb +23 -2
  46. data/lib/doorkeeper/orm/active_record/application.rb +20 -2
  47. data/lib/doorkeeper/secret_storing/base.rb +63 -0
  48. data/lib/doorkeeper/secret_storing/bcrypt.rb +59 -0
  49. data/lib/doorkeeper/secret_storing/plain.rb +33 -0
  50. data/lib/doorkeeper/secret_storing/sha256_hash.rb +25 -0
  51. data/lib/doorkeeper/version.rb +3 -3
  52. data/lib/doorkeeper.rb +7 -0
  53. data/lib/generators/doorkeeper/templates/initializer.rb +90 -8
  54. data/spec/controllers/application_metal_controller_spec.rb +18 -4
  55. data/spec/controllers/authorizations_controller_spec.rb +3 -3
  56. data/spec/controllers/token_info_controller_spec.rb +1 -1
  57. data/spec/controllers/tokens_controller_spec.rb +85 -41
  58. data/spec/dummy/app/controllers/application_controller.rb +1 -1
  59. data/spec/dummy/config/application.rb +12 -1
  60. data/spec/factories.rb +3 -3
  61. data/spec/lib/config_spec.rb +168 -0
  62. data/spec/lib/models/expirable_spec.rb +12 -0
  63. data/spec/lib/models/reusable_spec.rb +40 -0
  64. data/spec/lib/models/scopes_spec.rb +13 -1
  65. data/spec/lib/models/secret_storable_spec.rb +113 -0
  66. data/spec/lib/oauth/authorization_code_request_spec.rb +18 -1
  67. data/spec/lib/oauth/base_request_spec.rb +7 -7
  68. data/spec/lib/oauth/client_credentials/creator_spec.rb +51 -7
  69. data/spec/lib/oauth/client_credentials/validation_spec.rb +3 -0
  70. data/spec/lib/oauth/error_response_spec.rb +7 -1
  71. data/spec/lib/oauth/helpers/scope_checker_spec.rb +52 -17
  72. data/spec/lib/oauth/helpers/uri_checker_spec.rb +20 -2
  73. data/spec/lib/oauth/password_access_token_request_spec.rb +43 -12
  74. data/spec/lib/oauth/pre_authorization_spec.rb +24 -0
  75. data/spec/lib/oauth/token_request_spec.rb +16 -1
  76. data/spec/lib/oauth/token_response_spec.rb +13 -13
  77. data/spec/lib/oauth/token_spec.rb +14 -0
  78. data/spec/lib/secret_storing/base_spec.rb +60 -0
  79. data/spec/lib/secret_storing/bcrypt_spec.rb +49 -0
  80. data/spec/lib/secret_storing/plain_spec.rb +44 -0
  81. data/spec/lib/secret_storing/sha256_hash_spec.rb +48 -0
  82. data/spec/models/doorkeeper/access_grant_spec.rb +61 -0
  83. data/spec/models/doorkeeper/access_token_spec.rb +123 -0
  84. data/spec/models/doorkeeper/application_spec.rb +56 -0
  85. data/spec/requests/flows/authorization_code_spec.rb +41 -1
  86. data/spec/requests/flows/client_credentials_spec.rb +2 -2
  87. data/spec/requests/flows/implicit_grant_spec.rb +1 -1
  88. data/spec/requests/flows/password_spec.rb +7 -5
  89. data/spec/requests/flows/revoke_token_spec.rb +14 -30
  90. data/spec/routing/custom_controller_routes_spec.rb +4 -0
  91. data/spec/spec_helper.rb +2 -1
  92. data/spec/support/ruby_2_6_rails_4_2_patch.rb +14 -0
  93. data/spec/support/shared/hashing_shared_context.rb +36 -0
  94. 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 == 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
@@ -4,7 +4,11 @@ module Doorkeeper
4
4
  module Models
5
5
  module Scopes
6
6
  def scopes
7
- OAuth::Scopes.from_string(self[:scopes])
7
+ OAuth::Scopes.from_string(scopes_string)
8
+ end
9
+
10
+ def scopes=(value)
11
+ super Array(value).join(' ')
8
12
  end
9
13
 
10
14
  def scopes_string
@@ -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
@@ -16,7 +16,7 @@ module Doorkeeper
16
16
  end
17
17
 
18
18
  def native_redirect
19
- { action: :show, code: token.token }
19
+ { action: :show, code: token.plaintext_token }
20
20
  end
21
21
 
22
22
  def configuration
@@ -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?(:client)
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.token
67
+ access_token: token.plaintext_token
66
68
  }
67
69
  end
68
70
 
@@ -49,7 +49,7 @@ module Doorkeeper
49
49
  end
50
50
 
51
51
  def validate_client
52
- !client.nil?
52
+ client.present?
53
53
  end
54
54
 
55
55
  def validate_grant
@@ -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 false if credentials.blank?
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.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.token,
34
+ code: auth.token.plaintext_token,
35
35
  state: pre_auth.state
36
36
  )
37
37
  end
@@ -32,7 +32,11 @@ module Doorkeeper
32
32
  end
33
33
 
34
34
  def status
35
- :unauthorized
35
+ if name == :invalid_client
36
+ :unauthorized
37
+ else
38
+ :bad_request
39
+ end
36
40
  end
37
41
 
38
42
  def redirectable?
@@ -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, application_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, application_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, application_scopes)
25
- if application_scopes.present?
26
- application_scopes
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, server_scopes, application_scopes = nil)
34
- Validator.new(scope_str, server_scopes, application_scopes).valid?
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
- generator_method = options.delete(:generator) || SecureRandom.method(:hex)
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
@@ -22,6 +22,10 @@ module Doorkeeper
22
22
  @reason = attributes[:reason] || :unknown
23
23
  end
24
24
 
25
+ def status
26
+ :unauthorized
27
+ end
28
+
25
29
  def description
26
30
  scope = { scope: %i[doorkeeper errors messages invalid_token] }
27
31
  @description ||= I18n.translate @reason, scope
@@ -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?(scopes.to_s, server.scopes, client_scopes)
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] || !client.nil?
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?(@original_scopes, refresh_token.scopes)
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
- else
60
+ elsif authorized_token
45
61
  # Requested bearer token authorization
46
- @error = :invalid_request unless authorized_token
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.present? && @token.accessible?
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.present?
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.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.refresh_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? }