doorkeeper 5.1.0.rc1 → 5.1.0.rc2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of doorkeeper might be problematic. Click here for more details.

Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +11 -2
  3. data/Appraisals +29 -3
  4. data/Gemfile +13 -5
  5. data/NEWS.md +52 -15
  6. data/README.md +68 -487
  7. data/app/controllers/doorkeeper/token_info_controller.rb +1 -1
  8. data/app/controllers/doorkeeper/tokens_controller.rb +1 -1
  9. data/doorkeeper.gemspec +3 -2
  10. data/gemfiles/rails_4_2.gemfile +8 -5
  11. data/gemfiles/rails_5_0.gemfile +9 -6
  12. data/gemfiles/rails_5_1.gemfile +9 -6
  13. data/gemfiles/rails_5_2.gemfile +9 -6
  14. data/gemfiles/rails_6_0.gemfile +16 -0
  15. data/gemfiles/rails_master.gemfile +8 -10
  16. data/lib/doorkeeper.rb +7 -1
  17. data/lib/doorkeeper/config.rb +110 -24
  18. data/lib/doorkeeper/models/access_grant_mixin.rb +15 -7
  19. data/lib/doorkeeper/models/access_token_mixin.rb +29 -16
  20. data/lib/doorkeeper/models/application_mixin.rb +18 -28
  21. data/lib/doorkeeper/models/concerns/expirable.rb +3 -2
  22. data/lib/doorkeeper/models/concerns/reusable.rb +19 -0
  23. data/lib/doorkeeper/models/concerns/scopes.rb +4 -0
  24. data/lib/doorkeeper/models/concerns/secret_storable.rb +106 -0
  25. data/lib/doorkeeper/oauth/authorization/token.rb +3 -1
  26. data/lib/doorkeeper/oauth/error_response.rb +5 -1
  27. data/lib/doorkeeper/oauth/helpers/unique_token.rb +12 -1
  28. data/lib/doorkeeper/oauth/invalid_token_response.rb +4 -0
  29. data/lib/doorkeeper/oauth/token_introspection.rb +72 -6
  30. data/lib/doorkeeper/orm/active_record/access_grant.rb +9 -8
  31. data/lib/doorkeeper/orm/active_record/application.rb +10 -6
  32. data/lib/doorkeeper/secret_storing/base.rb +63 -0
  33. data/lib/doorkeeper/secret_storing/bcrypt.rb +59 -0
  34. data/lib/doorkeeper/secret_storing/plain.rb +33 -0
  35. data/lib/doorkeeper/secret_storing/sha256_hash.rb +25 -0
  36. data/lib/doorkeeper/version.rb +1 -1
  37. data/lib/generators/doorkeeper/templates/initializer.rb +62 -20
  38. data/spec/controllers/authorizations_controller_spec.rb +3 -3
  39. data/spec/controllers/token_info_controller_spec.rb +1 -1
  40. data/spec/controllers/tokens_controller_spec.rb +78 -30
  41. data/spec/dummy/config/application.rb +12 -1
  42. data/spec/lib/config_spec.rb +119 -35
  43. data/spec/lib/models/expirable_spec.rb +12 -0
  44. data/spec/lib/models/reusable_spec.rb +40 -0
  45. data/spec/lib/models/scopes_spec.rb +13 -1
  46. data/spec/lib/models/secret_storable_spec.rb +113 -0
  47. data/spec/lib/oauth/authorization_code_request_spec.rb +18 -1
  48. data/spec/lib/oauth/client_credentials/creator_spec.rb +51 -7
  49. data/spec/lib/oauth/error_response_spec.rb +7 -1
  50. data/spec/lib/oauth/password_access_token_request_spec.rb +11 -1
  51. data/spec/lib/oauth/token_request_spec.rb +16 -1
  52. data/spec/lib/secret_storing/base_spec.rb +60 -0
  53. data/spec/lib/secret_storing/bcrypt_spec.rb +49 -0
  54. data/spec/lib/secret_storing/plain_spec.rb +44 -0
  55. data/spec/lib/secret_storing/sha256_hash_spec.rb +48 -0
  56. data/spec/models/doorkeeper/application_spec.rb +23 -4
  57. data/spec/requests/flows/authorization_code_spec.rb +3 -3
  58. data/spec/requests/flows/client_credentials_spec.rb +2 -2
  59. data/spec/requests/flows/implicit_grant_spec.rb +1 -1
  60. data/spec/requests/flows/password_spec.rb +3 -3
  61. data/spec/routing/custom_controller_routes_spec.rb +4 -0
  62. data/spec/support/shared/hashing_shared_context.rb +12 -5
  63. metadata +51 -21
  64. data/lib/doorkeeper/models/concerns/hashable.rb +0 -137
  65. data/spec/lib/models/hashable_spec.rb +0 -183
@@ -1,40 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bcrypt'
4
-
5
3
  module Doorkeeper
6
4
  module ApplicationMixin
7
5
  extend ActiveSupport::Concern
8
6
 
9
7
  include OAuth::Helpers
10
8
  include Models::Orderable
11
- include Models::Hashable
9
+ include Models::SecretStorable
12
10
  include Models::Scopes
13
11
 
14
- included do
15
- # Use BCrypt as the hashing function for applications
16
- self.secret_hash_function = lambda do |plain_token|
17
- BCrypt::Password.create(plain_token.to_s)
18
- end
19
-
20
- # Also need to override the comparer function for BCrypt
21
- self.secret_comparer = lambda do |plain, secret|
22
- begin
23
- BCrypt::Password.new(secret.to_s) == plain.to_s
24
- rescue BCrypt::Errors::InvalidHash
25
- false
26
- end
27
- end
28
- end
29
-
30
12
  # :nodoc
31
13
  module ClassMethods
32
- # We want to perform secret hashing whenever the user
33
- # enables the configuration option +hash_application_secrets+
34
- def perform_secret_hashing?
35
- Doorkeeper.configuration.hash_application_secrets?
36
- end
37
-
38
14
  # Returns an instance of the Doorkeeper::Application with
39
15
  # specific UID and secret.
40
16
  #
@@ -65,6 +41,20 @@ module Doorkeeper
65
41
  def by_uid(uid)
66
42
  find_by(uid: uid.to_s)
67
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
68
58
  end
69
59
 
70
60
  # Set an application's valid redirect URIs.
@@ -90,12 +80,12 @@ module Doorkeeper
90
80
  return false if input.nil? || secret.nil?
91
81
 
92
82
  # When matching the secret by comparer function, all is well.
93
- return true if self.class.secret_matches?(input, secret)
83
+ return true if secret_strategy.secret_matches?(input, secret)
94
84
 
95
85
  # When fallback lookup is enabled, ensure applications
96
86
  # with plain secrets can still be found
97
- if Doorkeeper.configuration.fallback_to_plain_secrets?
98
- ActiveSupport::SecurityUtils.secure_compare(input, secret)
87
+ if fallback_secret_strategy
88
+ fallback_secret_strategy.secret_matches?(input, secret)
99
89
  else
100
90
  false
101
91
  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
@@ -7,6 +7,10 @@ module Doorkeeper
7
7
  OAuth::Scopes.from_string(scopes_string)
8
8
  end
9
9
 
10
+ def scopes=(value)
11
+ super Array(value).join(' ')
12
+ end
13
+
10
14
  def scopes_string
11
15
  self[:scopes]
12
16
  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?(: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
@@ -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?
@@ -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
@@ -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
@@ -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
@@ -27,16 +27,17 @@ module Doorkeeper
27
27
 
28
28
  before_validation :generate_token, on: :create
29
29
 
30
- # Keep a reference to the generated token during generation
31
- # of this access grant. The actual token may be mapped by
32
- # the configuration hasher and may not be available in plaintext.
30
+ # We keep a volatile copy of the raw token for initial communication
31
+ # The stored refresh_token may be mapped and not available in cleartext.
33
32
  #
34
- # If hash tokens are enabled, this will return nil on fetched tokens
33
+ # Some strategies allow restoring stored secrets (e.g. symmetric encryption)
34
+ # while hashing strategies do not, so you cannot rely on this value
35
+ # returning a present value for persisted tokens.
35
36
  def plaintext_token
36
- if perform_secret_hashing?
37
- @raw_token
37
+ if secret_strategy.allows_restoring_secrets?
38
+ secret_strategy.restore_secret(self, :token)
38
39
  else
39
- token
40
+ @raw_token
40
41
  end
41
42
  end
42
43
 
@@ -48,7 +49,7 @@ module Doorkeeper
48
49
  #
49
50
  def generate_token
50
51
  @raw_token = UniqueToken.generate
51
- self.token = hashed_or_plain_token(@raw_token)
52
+ secret_strategy.store_secret(self, :token, @raw_token)
52
53
  end
53
54
  end
54
55
  end