doorkeeper 5.0.3 → 5.1.0.rc1

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 +7 -3
  3. data/Dangerfile +5 -2
  4. data/Gemfile +3 -1
  5. data/NEWS.md +20 -13
  6. data/README.md +1 -1
  7. data/app/controllers/doorkeeper/applications_controller.rb +3 -3
  8. data/app/controllers/doorkeeper/authorized_applications_controller.rb +1 -1
  9. data/app/controllers/doorkeeper/tokens_controller.rb +6 -6
  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/gemfiles/rails_4_2.gemfile +1 -0
  14. data/gemfiles/rails_5_0.gemfile +1 -0
  15. data/gemfiles/rails_5_1.gemfile +1 -0
  16. data/gemfiles/rails_5_2.gemfile +2 -1
  17. data/gemfiles/rails_master.gemfile +1 -0
  18. data/lib/doorkeeper.rb +1 -0
  19. data/lib/doorkeeper/config.rb +73 -6
  20. data/lib/doorkeeper/helpers/controller.rb +3 -2
  21. data/lib/doorkeeper/models/access_grant_mixin.rb +8 -1
  22. data/lib/doorkeeper/models/access_token_mixin.rb +40 -9
  23. data/lib/doorkeeper/models/application_mixin.rb +52 -1
  24. data/lib/doorkeeper/models/concerns/hashable.rb +137 -0
  25. data/lib/doorkeeper/models/concerns/scopes.rb +1 -1
  26. data/lib/doorkeeper/oauth/authorization/code.rb +1 -1
  27. data/lib/doorkeeper/oauth/authorization/token.rb +1 -1
  28. data/lib/doorkeeper/oauth/authorization_code_request.rb +1 -1
  29. data/lib/doorkeeper/oauth/client.rb +1 -1
  30. data/lib/doorkeeper/oauth/client_credentials/validation.rb +4 -3
  31. data/lib/doorkeeper/oauth/code_response.rb +2 -2
  32. data/lib/doorkeeper/oauth/helpers/scope_checker.rb +23 -8
  33. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +32 -0
  34. data/lib/doorkeeper/oauth/password_access_token_request.rb +7 -2
  35. data/lib/doorkeeper/oauth/pre_authorization.rb +8 -3
  36. data/lib/doorkeeper/oauth/refresh_token_request.rb +4 -1
  37. data/lib/doorkeeper/oauth/token_response.rb +2 -2
  38. data/lib/doorkeeper/orm/active_record/access_grant.rb +22 -2
  39. data/lib/doorkeeper/orm/active_record/application.rb +12 -53
  40. data/lib/doorkeeper/version.rb +3 -3
  41. data/lib/generators/doorkeeper/templates/initializer.rb +41 -1
  42. data/spec/controllers/application_metal_controller_spec.rb +18 -4
  43. data/spec/controllers/tokens_controller_spec.rb +7 -11
  44. data/spec/dummy/app/controllers/application_controller.rb +1 -1
  45. data/spec/factories.rb +3 -3
  46. data/spec/lib/config_spec.rb +84 -0
  47. data/spec/lib/models/hashable_spec.rb +183 -0
  48. data/spec/lib/oauth/base_request_spec.rb +7 -7
  49. data/spec/lib/oauth/client_credentials/validation_spec.rb +3 -0
  50. data/spec/lib/oauth/helpers/scope_checker_spec.rb +52 -17
  51. data/spec/lib/oauth/helpers/uri_checker_spec.rb +20 -2
  52. data/spec/lib/oauth/password_access_token_request_spec.rb +32 -11
  53. data/spec/lib/oauth/pre_authorization_spec.rb +24 -0
  54. data/spec/lib/oauth/token_response_spec.rb +13 -13
  55. data/spec/lib/oauth/token_spec.rb +14 -0
  56. data/spec/models/doorkeeper/access_grant_spec.rb +61 -0
  57. data/spec/models/doorkeeper/access_token_spec.rb +123 -0
  58. data/spec/models/doorkeeper/application_spec.rb +227 -295
  59. data/spec/requests/flows/authorization_code_spec.rb +40 -0
  60. data/spec/requests/flows/password_spec.rb +4 -2
  61. data/spec/requests/flows/revoke_token_spec.rb +14 -30
  62. data/spec/spec_helper.rb +2 -1
  63. data/spec/support/ruby_2_6_rails_4_2_patch.rb +14 -0
  64. data/spec/support/shared/hashing_shared_context.rb +29 -0
  65. metadata +12 -4
@@ -55,8 +55,9 @@ module Doorkeeper
55
55
  end
56
56
 
57
57
  def enforce_content_type
58
- return if request.content_type == 'application/x-www-form-urlencoded'
59
- render json: {}, status: :unsupported_media_type
58
+ if (request.put? || request.post? || request.patch?) && request.content_type != 'application/x-www-form-urlencoded'
59
+ render json: {}, status: :unsupported_media_type
60
+ end
60
61
  end
61
62
  end
62
63
  end
@@ -9,6 +9,7 @@ module Doorkeeper
9
9
  include Models::Revocable
10
10
  include Models::Accessible
11
11
  include Models::Orderable
12
+ include Models::Hashable
12
13
  include Models::Scopes
13
14
 
14
15
  # never uses pkce, if pkce migrations were not generated
@@ -30,7 +31,13 @@ module Doorkeeper
30
31
  # if there is no record with such token
31
32
  #
32
33
  def by_token(token)
33
- find_by(token: token.to_s)
34
+ find_by_plaintext_token(:token, token)
35
+ end
36
+
37
+ # We want to perform secret hashing whenever the user
38
+ # enables the configuration option +hash_token_secrets+
39
+ def perform_secret_hashing?
40
+ Doorkeeper.configuration.hash_token_secrets?
34
41
  end
35
42
 
36
43
  # Revokes AccessGrant records that have not been revoked and associated
@@ -9,20 +9,21 @@ module Doorkeeper
9
9
  include Models::Revocable
10
10
  include Models::Accessible
11
11
  include Models::Orderable
12
+ include Models::Hashable
12
13
  include Models::Scopes
13
14
 
14
15
  module ClassMethods
15
16
  # Returns an instance of the Doorkeeper::AccessToken with
16
- # specific token value.
17
+ # specific plain text token value.
17
18
  #
18
19
  # @param token [#to_s]
19
- # token value (any object that responds to `#to_s`)
20
+ # Plain text token value (any object that responds to `#to_s`)
20
21
  #
21
22
  # @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
22
23
  # if there is no record with such token
23
24
  #
24
25
  def by_token(token)
25
- find_by(token: token.to_s)
26
+ find_by_plaintext_token(:token, token)
26
27
  end
27
28
 
28
29
  # Returns an instance of the Doorkeeper::AccessToken
@@ -35,7 +36,13 @@ module Doorkeeper
35
36
  # if there is no record with such refresh token
36
37
  #
37
38
  def by_refresh_token(refresh_token)
38
- find_by(refresh_token: refresh_token.to_s)
39
+ find_by_plaintext_token(:refresh_token, refresh_token)
40
+ end
41
+
42
+ # We want to perform secret hashing whenever the user
43
+ # enables the configuration option +hash_token_secrets+
44
+ def perform_secret_hashing?
45
+ Doorkeeper.configuration.hash_token_secrets?
39
46
  end
40
47
 
41
48
  # Revokes AccessToken records that have not been revoked and associated
@@ -98,9 +105,9 @@ module Doorkeeper
98
105
 
99
106
  (token_scopes.sort == param_scopes.sort) &&
100
107
  Doorkeeper::OAuth::Helpers::ScopeChecker.valid?(
101
- param_scopes.to_s,
102
- Doorkeeper.configuration.scopes,
103
- app_scopes
108
+ scope_str: param_scopes.to_s,
109
+ server_scopes: Doorkeeper.configuration.scopes,
110
+ app_scopes: app_scopes
104
111
  )
105
112
  end
106
113
 
@@ -219,6 +226,26 @@ module Doorkeeper
219
226
  accessible? && includes_scope?(*scopes)
220
227
  end
221
228
 
229
+ # We keep a volatile copy of the raw refresh token for initial communication
230
+ # The stored refresh_token may be mapped and not available in cleartext.
231
+ def plaintext_refresh_token
232
+ if perform_secret_hashing?
233
+ @raw_refresh_token
234
+ else
235
+ refresh_token
236
+ end
237
+ end
238
+
239
+ # We keep a volatile copy of the raw token for initial communication
240
+ # The stored refresh_token may be mapped and not available in cleartext.
241
+ def plaintext_token
242
+ if perform_secret_hashing?
243
+ @raw_token
244
+ else
245
+ token
246
+ end
247
+ end
248
+
222
249
  private
223
250
 
224
251
  # Generates refresh token with UniqueToken generator.
@@ -226,7 +253,8 @@ module Doorkeeper
226
253
  # @return [String] refresh token value
227
254
  #
228
255
  def generate_refresh_token
229
- self.refresh_token = UniqueToken.generate
256
+ @raw_refresh_token = UniqueToken.generate
257
+ self.refresh_token = hashed_or_plain_token(@raw_refresh_token)
230
258
  end
231
259
 
232
260
  # Generates and sets the token value with the
@@ -242,13 +270,16 @@ module Doorkeeper
242
270
  def generate_token
243
271
  self.created_at ||= Time.now.utc
244
272
 
245
- self.token = token_generator.generate(
273
+ @raw_token = token_generator.generate(
246
274
  resource_owner_id: resource_owner_id,
247
275
  scopes: scopes,
248
276
  application: application,
249
277
  expires_in: expires_in,
250
278
  created_at: created_at
251
279
  )
280
+
281
+ self.token = hashed_or_plain_token(@raw_token)
282
+ @raw_token
252
283
  end
253
284
 
254
285
  def token_generator
@@ -1,14 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bcrypt'
4
+
3
5
  module Doorkeeper
4
6
  module ApplicationMixin
5
7
  extend ActiveSupport::Concern
6
8
 
7
9
  include OAuth::Helpers
8
10
  include Models::Orderable
11
+ include Models::Hashable
9
12
  include Models::Scopes
10
13
 
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
+ # :nodoc
11
31
  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
+
12
38
  # Returns an instance of the Doorkeeper::Application with
13
39
  # specific UID and secret.
14
40
  #
@@ -25,7 +51,7 @@ module Doorkeeper
25
51
  app = by_uid(uid)
26
52
  return unless app
27
53
  return app if secret.blank? && !app.confidential?
28
- return unless app.secret == secret
54
+ return unless app.secret_matches?(secret)
29
55
  app
30
56
  end
31
57
 
@@ -49,5 +75,30 @@ module Doorkeeper
49
75
  def redirect_uri=(uris)
50
76
  super(uris.is_a?(Array) ? uris.join("\n") : uris)
51
77
  end
78
+
79
+ # Check whether the given plain text secret matches our stored secret
80
+ #
81
+ # @param input [#to_s] Plain secret provided by user
82
+ # (any object that responds to `#to_s`)
83
+ #
84
+ # @return [true] Whether the given secret matches the stored secret
85
+ # of this application.
86
+ #
87
+ def secret_matches?(input)
88
+ # return false if either is nil, since secure_compare depends on strings
89
+ # but Application secrets MAY be nil depending on confidentiality.
90
+ return false if input.nil? || secret.nil?
91
+
92
+ # When matching the secret by comparer function, all is well.
93
+ return true if self.class.secret_matches?(input, secret)
94
+
95
+ # When fallback lookup is enabled, ensure applications
96
+ # with plain secrets can still be found
97
+ if Doorkeeper.configuration.fallback_to_plain_secrets?
98
+ ActiveSupport::SecurityUtils.secure_compare(input, secret)
99
+ else
100
+ false
101
+ end
102
+ end
52
103
  end
53
104
  end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module Models
5
+ ##
6
+ # Hashable finder to provide lookups for input plaintext values which are
7
+ # mapped to their hashes before lookup.
8
+ module Hashable
9
+ extend ActiveSupport::Concern
10
+
11
+ delegate :perform_secret_hashing?,
12
+ :hashed_or_plain_token,
13
+ to: :class
14
+
15
+ # :nodoc
16
+ module ClassMethods
17
+ # Allow to override the hashing method by the including module
18
+ attr_accessor :secret_hash_function, :secret_comparer
19
+
20
+ # Compare the given plaintext with the secret
21
+ #
22
+ # @param input [String]
23
+ # The plain input to compare.
24
+ #
25
+ # @param secret [String]
26
+ # The secret value to compare with.
27
+ #
28
+ # @return [Boolean]
29
+ # If hashing is enabled: Whether the secret equals hashed input
30
+ # If hashing is disabled: Whether input matches secret
31
+ #
32
+ def secret_matches?(input, secret)
33
+ unless perform_secret_hashing?
34
+ return ActiveSupport::SecurityUtils.secure_compare input, secret
35
+ end
36
+
37
+ (secret_comparer || method(:default_comparer))
38
+ .call(input, secret)
39
+ end
40
+
41
+ # Returns an instance of the Doorkeeper::AccessToken with
42
+ # specific token value.
43
+ #
44
+ # @param attr [Symbol]
45
+ # The token attribute we're looking with.
46
+ #
47
+ # @param token [#to_s]
48
+ # token value (any object that responds to `#to_s`)
49
+ #
50
+ # @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
51
+ # if there is no record with such token
52
+ #
53
+ def find_by_plaintext_token(attr, token)
54
+ token = token.to_s
55
+
56
+ find_by(attr => hashed_or_plain_token(token)) ||
57
+ find_by_fallback_token(attr, token)
58
+ end
59
+
60
+ # Allow looking up previously plain tokens as a fallback
61
+ # IFF respective options are enabled
62
+ #
63
+ # @param attr [Symbol]
64
+ # The token attribute we're looking with.
65
+ #
66
+ # @param token [#to_s]
67
+ # token value (any object that responds to `#to_s`)
68
+ #
69
+ # @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
70
+ # if there is no record with such token
71
+ #
72
+ def find_by_fallback_token(attr, token)
73
+ return nil unless perform_secret_hashing?
74
+ return nil unless Doorkeeper.configuration.fallback_to_plain_secrets?
75
+
76
+ find_by(attr => token).tap do |fallback|
77
+ upgrade_fallback_value fallback, attr
78
+ end
79
+ end
80
+
81
+ # Hash the given input token
82
+ #
83
+ # @param plain_token [String]
84
+ # The plain text token to hash.
85
+ #
86
+ # @return [String]
87
+ # IFF secret hashing enabled, the hashed token,
88
+ # otherwise returns the plain token.
89
+ def hashed_or_plain_token(plain_token)
90
+ if perform_secret_hashing?
91
+ (secret_hash_function || method(:default_hash_function))
92
+ .call plain_token
93
+ else
94
+ plain_token
95
+ end
96
+ end
97
+
98
+ # Allow implementations in ORMs to replace a plain
99
+ # value falling back to to avoid it remaining as plain text.
100
+ #
101
+ # @param instance
102
+ # An instance of this model with a plain value token.
103
+ #
104
+ # @param attr
105
+ # The token attribute to upgrade
106
+ #
107
+ def upgrade_fallback_value(instance, attr)
108
+ plain_token = instance.public_send attr
109
+ instance.update_column(attr, hashed_or_plain_token(plain_token))
110
+ end
111
+
112
+ # Including classes can override this function to
113
+ # disable or enable secret hashing dynamically
114
+ def perform_secret_hashing?
115
+ true
116
+ end
117
+
118
+ # Return a default hashing function to be used when including
119
+ # module or user does not specify what to use
120
+ # @param plain_token [String]
121
+ # The plain text token to hash.
122
+ #
123
+ # @return [String] Hashed plain text token
124
+ #
125
+ def default_hash_function(plain_token)
126
+ ::Digest::SHA256.hexdigest plain_token
127
+ end
128
+
129
+ # Return a default comparer for the given hash function
130
+ def default_comparer(plain, secret)
131
+ hashed = hashed_or_plain_token(plain)
132
+ ActiveSupport::SecurityUtils.secure_compare hashed, secret
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -4,7 +4,7 @@ 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
8
  end
9
9
 
10
10
  def scopes_string
@@ -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
@@ -62,7 +62,7 @@ module Doorkeeper
62
62
  {
63
63
  controller: controller,
64
64
  action: :show,
65
- access_token: token.token
65
+ access_token: token.plaintext_token
66
66
  }
67
67
  end
68
68
 
@@ -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
@@ -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