doorkeeper 5.0.0 → 5.1.0.rc1

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 (160) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +12 -3
  3. data/Dangerfile +67 -0
  4. data/Gemfile +3 -1
  5. data/NEWS.md +49 -5
  6. data/README.md +19 -4
  7. data/app/controllers/doorkeeper/application_controller.rb +2 -0
  8. data/app/controllers/doorkeeper/application_metal_controller.rb +2 -0
  9. data/app/controllers/doorkeeper/applications_controller.rb +4 -2
  10. data/app/controllers/doorkeeper/authorizations_controller.rb +3 -3
  11. data/app/controllers/doorkeeper/authorized_applications_controller.rb +2 -0
  12. data/app/controllers/doorkeeper/token_info_controller.rb +2 -0
  13. data/app/controllers/doorkeeper/tokens_controller.rb +8 -6
  14. data/app/helpers/doorkeeper/dashboard_helper.rb +2 -0
  15. data/app/validators/redirect_uri_validator.rb +2 -0
  16. data/app/views/doorkeeper/applications/show.html.erb +1 -1
  17. data/app/views/layouts/doorkeeper/admin.html.erb +5 -3
  18. data/bin/console +15 -0
  19. data/doorkeeper.gemspec +23 -22
  20. data/gemfiles/rails_4_2.gemfile +1 -0
  21. data/gemfiles/rails_5_0.gemfile +1 -0
  22. data/gemfiles/rails_5_1.gemfile +1 -0
  23. data/gemfiles/rails_5_2.gemfile +2 -1
  24. data/gemfiles/rails_master.gemfile +1 -0
  25. data/lib/doorkeeper/config.rb +80 -8
  26. data/lib/doorkeeper/engine.rb +2 -0
  27. data/lib/doorkeeper/errors.rb +17 -0
  28. data/lib/doorkeeper/grape/authorization_decorator.rb +2 -0
  29. data/lib/doorkeeper/grape/helpers.rb +2 -0
  30. data/lib/doorkeeper/helpers/controller.rb +5 -2
  31. data/lib/doorkeeper/models/access_grant_mixin.rb +13 -4
  32. data/lib/doorkeeper/models/access_token_mixin.rb +45 -12
  33. data/lib/doorkeeper/models/application_mixin.rb +54 -1
  34. data/lib/doorkeeper/models/concerns/accessible.rb +2 -0
  35. data/lib/doorkeeper/models/concerns/expirable.rb +2 -0
  36. data/lib/doorkeeper/models/concerns/hashable.rb +137 -0
  37. data/lib/doorkeeper/models/concerns/orderable.rb +2 -0
  38. data/lib/doorkeeper/models/concerns/ownership.rb +2 -0
  39. data/lib/doorkeeper/models/concerns/revocable.rb +2 -0
  40. data/lib/doorkeeper/models/concerns/scopes.rb +3 -1
  41. data/lib/doorkeeper/oauth/authorization/code.rb +3 -1
  42. data/lib/doorkeeper/oauth/authorization/context.rb +2 -0
  43. data/lib/doorkeeper/oauth/authorization/token.rb +3 -1
  44. data/lib/doorkeeper/oauth/authorization/uri_builder.rb +2 -0
  45. data/lib/doorkeeper/oauth/authorization_code_request.rb +3 -1
  46. data/lib/doorkeeper/oauth/base_request.rb +2 -0
  47. data/lib/doorkeeper/oauth/base_response.rb +2 -0
  48. data/lib/doorkeeper/oauth/client/credentials.rb +2 -0
  49. data/lib/doorkeeper/oauth/client.rb +3 -1
  50. data/lib/doorkeeper/oauth/client_credentials/creator.rb +2 -0
  51. data/lib/doorkeeper/oauth/client_credentials/issuer.rb +2 -0
  52. data/lib/doorkeeper/oauth/client_credentials/validation.rb +6 -3
  53. data/lib/doorkeeper/oauth/client_credentials_request.rb +2 -0
  54. data/lib/doorkeeper/oauth/code_request.rb +2 -0
  55. data/lib/doorkeeper/oauth/code_response.rb +4 -2
  56. data/lib/doorkeeper/oauth/error.rb +2 -0
  57. data/lib/doorkeeper/oauth/error_response.rb +10 -0
  58. data/lib/doorkeeper/oauth/forbidden_token_response.rb +9 -2
  59. data/lib/doorkeeper/oauth/helpers/scope_checker.rb +25 -8
  60. data/lib/doorkeeper/oauth/helpers/unique_token.rb +2 -0
  61. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +37 -2
  62. data/lib/doorkeeper/oauth/invalid_token_response.rb +18 -0
  63. data/lib/doorkeeper/oauth/password_access_token_request.rb +9 -2
  64. data/lib/doorkeeper/oauth/pre_authorization.rb +10 -3
  65. data/lib/doorkeeper/oauth/refresh_token_request.rb +14 -3
  66. data/lib/doorkeeper/oauth/scopes.rb +2 -0
  67. data/lib/doorkeeper/oauth/token.rb +2 -0
  68. data/lib/doorkeeper/oauth/token_introspection.rb +2 -0
  69. data/lib/doorkeeper/oauth/token_request.rb +2 -0
  70. data/lib/doorkeeper/oauth/token_response.rb +4 -2
  71. data/lib/doorkeeper/orm/active_record/access_grant.rb +22 -2
  72. data/lib/doorkeeper/orm/active_record/application.rb +16 -2
  73. data/lib/doorkeeper/orm/active_record.rb +2 -0
  74. data/lib/doorkeeper/rails/helpers.rb +4 -0
  75. data/lib/doorkeeper/rails/routes/mapper.rb +2 -0
  76. data/lib/doorkeeper/rails/routes/mapping.rb +2 -0
  77. data/lib/doorkeeper/rails/routes.rb +9 -2
  78. data/lib/doorkeeper/rake/db.rake +4 -4
  79. data/lib/doorkeeper/request/authorization_code.rb +2 -0
  80. data/lib/doorkeeper/request/client_credentials.rb +2 -0
  81. data/lib/doorkeeper/request/code.rb +2 -0
  82. data/lib/doorkeeper/request/password.rb +2 -0
  83. data/lib/doorkeeper/request/refresh_token.rb +2 -0
  84. data/lib/doorkeeper/request/strategy.rb +2 -0
  85. data/lib/doorkeeper/request/token.rb +2 -0
  86. data/lib/doorkeeper/request.rb +2 -0
  87. data/lib/doorkeeper/server.rb +2 -0
  88. data/lib/doorkeeper/stale_records_cleaner.rb +20 -0
  89. data/lib/doorkeeper/validations.rb +2 -0
  90. data/lib/doorkeeper/version.rb +4 -2
  91. data/lib/doorkeeper.rb +2 -0
  92. data/lib/generators/doorkeeper/templates/initializer.rb +61 -3
  93. data/lib/generators/doorkeeper/templates/migration.rb.erb +2 -3
  94. data/spec/controllers/application_metal_controller_spec.rb +18 -4
  95. data/spec/controllers/applications_controller_spec.rb +37 -41
  96. data/spec/controllers/authorizations_controller_spec.rb +71 -18
  97. data/spec/controllers/protected_resources_controller_spec.rb +44 -2
  98. data/spec/controllers/tokens_controller_spec.rb +11 -16
  99. data/spec/dummy/Rakefile +1 -1
  100. data/spec/dummy/app/controllers/application_controller.rb +1 -1
  101. data/spec/dummy/app/controllers/custom_authorizations_controller.rb +1 -1
  102. data/spec/dummy/app/controllers/home_controller.rb +1 -2
  103. data/spec/dummy/config/application.rb +1 -1
  104. data/spec/dummy/config/boot.rb +2 -4
  105. data/spec/dummy/config/environment.rb +1 -1
  106. data/spec/dummy/config/environments/test.rb +1 -1
  107. data/spec/dummy/config/initializers/doorkeeper.rb +2 -1
  108. data/spec/dummy/config/initializers/new_framework_defaults.rb +1 -3
  109. data/spec/dummy/config/initializers/secret_token.rb +1 -1
  110. data/spec/dummy/config.ru +1 -1
  111. data/spec/dummy/db/migrate/20151223192035_create_doorkeeper_tables.rb +4 -4
  112. data/spec/dummy/db/migrate/20151223200000_add_owner_to_application.rb +1 -1
  113. data/spec/dummy/script/rails +4 -3
  114. data/spec/factories.rb +9 -9
  115. data/spec/generators/install_generator_spec.rb +4 -1
  116. data/spec/generators/templates/routes.rb +0 -1
  117. data/spec/generators/views_generator_spec.rb +1 -1
  118. data/spec/grape/grape_integration_spec.rb +1 -1
  119. data/spec/lib/config_spec.rb +109 -8
  120. data/spec/lib/doorkeeper_spec.rb +5 -5
  121. data/spec/lib/models/hashable_spec.rb +183 -0
  122. data/spec/lib/oauth/authorization_code_request_spec.rb +9 -6
  123. data/spec/lib/oauth/base_request_spec.rb +10 -10
  124. data/spec/lib/oauth/client/credentials_spec.rb +2 -2
  125. data/spec/lib/oauth/client_credentials/issuer_spec.rb +0 -2
  126. data/spec/lib/oauth/client_credentials/validation_spec.rb +5 -1
  127. data/spec/lib/oauth/client_credentials_integration_spec.rb +1 -1
  128. data/spec/lib/oauth/code_request_spec.rb +2 -2
  129. data/spec/lib/oauth/code_response_spec.rb +1 -1
  130. data/spec/lib/oauth/helpers/scope_checker_spec.rb +52 -17
  131. data/spec/lib/oauth/helpers/uri_checker_spec.rb +37 -8
  132. data/spec/lib/oauth/password_access_token_request_spec.rb +48 -15
  133. data/spec/lib/oauth/pre_authorization_spec.rb +24 -0
  134. data/spec/lib/oauth/refresh_token_request_spec.rb +11 -7
  135. data/spec/lib/oauth/token_request_spec.rb +5 -5
  136. data/spec/lib/oauth/token_response_spec.rb +13 -13
  137. data/spec/lib/oauth/token_spec.rb +18 -1
  138. data/spec/lib/server_spec.rb +6 -6
  139. data/spec/lib/{orm/active_record/stale_records_cleaner_spec.rb → stale_records_cleaner_spec.rb} +14 -4
  140. data/spec/models/doorkeeper/access_grant_spec.rb +61 -0
  141. data/spec/models/doorkeeper/access_token_spec.rb +137 -10
  142. data/spec/models/doorkeeper/application_spec.rb +41 -4
  143. data/spec/requests/applications/applications_request_spec.rb +2 -2
  144. data/spec/requests/endpoints/authorization_spec.rb +2 -2
  145. data/spec/requests/flows/authorization_code_errors_spec.rb +1 -1
  146. data/spec/requests/flows/authorization_code_spec.rb +115 -15
  147. data/spec/requests/flows/implicit_grant_errors_spec.rb +2 -2
  148. data/spec/requests/flows/password_spec.rb +10 -4
  149. data/spec/requests/flows/refresh_token_spec.rb +57 -0
  150. data/spec/requests/flows/revoke_token_spec.rb +23 -39
  151. data/spec/requests/protected_resources/private_api_spec.rb +2 -2
  152. data/spec/spec_helper.rb +2 -1
  153. data/spec/support/doorkeeper_rspec.rb +2 -1
  154. data/spec/support/helpers/model_helper.rb +8 -4
  155. data/spec/support/helpers/url_helper.rb +11 -11
  156. data/spec/support/ruby_2_6_rails_4_2_patch.rb +14 -0
  157. data/spec/support/shared/controllers_shared_context.rb +56 -0
  158. data/spec/support/shared/hashing_shared_context.rb +29 -0
  159. data/spec/validators/redirect_uri_validator_spec.rb +2 -2
  160. metadata +31 -8
@@ -46,6 +46,7 @@ module Doorkeeper
46
46
  end
47
47
 
48
48
  def build
49
+ @config.validate
49
50
  @config
50
51
  end
51
52
 
@@ -83,6 +84,13 @@ module Doorkeeper
83
84
  @config.instance_variable_set(:@optional_scopes, OAuth::Scopes.from_array(scopes))
84
85
  end
85
86
 
87
+ # Define scopes_by_grant_type to limit certain scope to certain grant_type
88
+ # @param { Hash } with grant_types as keys.
89
+ # Default set to {} i.e. no limitation on scopes usage
90
+ def scopes_by_grant_type(hash = {})
91
+ @config.instance_variable_set(:@scopes_by_grant_type, hash)
92
+ end
93
+
86
94
  # Change the way client credentials are retrieved from the request object.
87
95
  # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
88
96
  # falls back to the `:client_id` and `:client_secret` params from the
@@ -107,7 +115,7 @@ module Doorkeeper
107
115
  def use_refresh_token(enabled = true, &block)
108
116
  @config.instance_variable_set(
109
117
  :@refresh_token_enabled,
110
- block ? block : enabled
118
+ block || enabled
111
119
  )
112
120
  end
113
121
 
@@ -136,6 +144,24 @@ module Doorkeeper
136
144
  def enforce_content_type
137
145
  @config.instance_variable_set(:@enforce_content_type, true)
138
146
  end
147
+
148
+ # Allow optional hashing of input tokens before persisting them.
149
+ # Will be used for hashing of input token and grants.
150
+ def hash_token_secrets
151
+ @config.instance_variable_set(:@hash_token_secrets, true)
152
+ end
153
+
154
+ # Allow optional hashing of application secrets before persisting them.
155
+ # Will be used for hashing of input token and grants.
156
+ def hash_application_secrets
157
+ @config.instance_variable_set(:@hash_application_secrets, true)
158
+ end
159
+
160
+ # Allow plain value lookup when using +hash_token_secrets+
161
+ # or +hash_application_secrets+ to avoid disrupting application experience
162
+ def fallback_to_plain_secrets
163
+ @config.instance_variable_set(:@fallback_to_plain_secrets, true)
164
+ end
139
165
  end
140
166
 
141
167
  module Option
@@ -177,7 +203,7 @@ module Doorkeeper
177
203
  value = if attribute_builder
178
204
  attribute_builder.new(&block).build
179
205
  else
180
- block ? block : args.first
206
+ block || args.first
181
207
  end
182
208
 
183
209
  @config.instance_variable_set(:"@#{attribute}", value)
@@ -239,6 +265,7 @@ module Doorkeeper
239
265
  option :native_redirect_uri, default: 'urn:ietf:wg:oauth:2.0:oob'
240
266
  option :active_record_options, default: {}
241
267
  option :grant_flows, default: %w[authorization_code client_credentials]
268
+ option :handle_auth_errors, default: :render
242
269
 
243
270
  # Allows to forbid specific Application redirect URI's by custom rules.
244
271
  # Doesn't forbid any URI by default.
@@ -285,9 +312,14 @@ module Doorkeeper
285
312
  option :base_controller,
286
313
  default: 'ActionController::Base'
287
314
 
288
- attr_reader :reuse_access_token
289
- attr_reader :api_only
290
- attr_reader :enforce_content_type
315
+ attr_reader :api_only,
316
+ :enforce_content_type,
317
+ :reuse_access_token
318
+
319
+ # Return the valid subset of this configuration
320
+ def validate
321
+ validate_reuse_access_token_value
322
+ end
291
323
 
292
324
  def api_only
293
325
  @api_only ||= false
@@ -306,15 +338,31 @@ module Doorkeeper
306
338
  end
307
339
 
308
340
  def enforce_configured_scopes?
309
- !!(defined?(@enforce_configured_scopes) && @enforce_configured_scopes)
341
+ option_set? :enforce_configured_scopes
310
342
  end
311
343
 
312
344
  def enable_application_owner?
313
- !!(defined?(@enable_application_owner) && @enable_application_owner)
345
+ option_set? :enable_application_owner
314
346
  end
315
347
 
316
348
  def confirm_application_owner?
317
- !!(defined?(@confirm_application_owner) && @confirm_application_owner)
349
+ option_set? :confirm_application_owner
350
+ end
351
+
352
+ def raise_on_errors?
353
+ handle_auth_errors == :raise
354
+ end
355
+
356
+ def hash_token_secrets?
357
+ option_set? :hash_token_secrets
358
+ end
359
+
360
+ def hash_application_secrets?
361
+ option_set? :hash_application_secrets
362
+ end
363
+
364
+ def fallback_to_plain_secrets?
365
+ option_set? :fallback_to_plain_secrets
318
366
  end
319
367
 
320
368
  def default_scopes
@@ -329,6 +377,10 @@ module Doorkeeper
329
377
  @scopes ||= default_scopes + optional_scopes
330
378
  end
331
379
 
380
+ def scopes_by_grant_type
381
+ @scopes_by_grant_type ||= {}
382
+ end
383
+
332
384
  def client_credentials_methods
333
385
  @client_credentials_methods ||= %i[from_basic from_params]
334
386
  end
@@ -347,6 +399,12 @@ module Doorkeeper
347
399
 
348
400
  private
349
401
 
402
+ # Helper to read boolearized configuration option
403
+ def option_set?(instance_key)
404
+ var = instance_variable_get("@#{instance_key}")
405
+ !!(defined?(var) && var)
406
+ end
407
+
350
408
  # Determines what values are acceptable for 'response_type' param in
351
409
  # authorization request endpoint, and return them as an array of strings.
352
410
  #
@@ -365,5 +423,19 @@ module Doorkeeper
365
423
  types << 'refresh_token' if refresh_token_enabled?
366
424
  types
367
425
  end
426
+
427
+ # Determine whether +reuse_access_token+ and +hash_token_secrets+
428
+ # have both been activated.
429
+ #
430
+ # In that case, disable reuse_access_token value and warn the user.
431
+ def validate_reuse_access_token_value
432
+ return unless hash_token_secrets? && reuse_access_token
433
+
434
+ ::Rails.logger.warn(
435
+ 'You are configured both reuse_access_token AND hash_token_secrets. ' \
436
+ 'This combination is unsupported. reuse_access_token will be disabled'
437
+ )
438
+ @reuse_access_token = false
439
+ end
368
440
  end
369
441
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  class Engine < Rails::Engine
3
5
  initializer "doorkeeper.params.filter" do |app|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module Errors
3
5
  class DoorkeeperError < StandardError
@@ -36,7 +38,22 @@ module Doorkeeper
36
38
  end
37
39
  end
38
40
 
41
+ class BaseResponseError < DoorkeeperError
42
+ attr_reader :response
43
+
44
+ def initialize(response)
45
+ @response = response
46
+ end
47
+ end
48
+
39
49
  UnableToGenerateToken = Class.new(DoorkeeperError)
40
50
  TokenGeneratorNotFound = Class.new(DoorkeeperError)
51
+ NoOrmCleaner = Class.new(DoorkeeperError)
52
+
53
+ InvalidToken = Class.new BaseResponseError
54
+ TokenExpired = Class.new InvalidToken
55
+ TokenRevoked = Class.new InvalidToken
56
+ TokenUnknown = Class.new InvalidToken
57
+ TokenForbidden = Class.new InvalidToken
41
58
  end
42
59
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module Grape
3
5
  class AuthorizationDecorator < SimpleDelegator
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'doorkeeper/grape/authorization_decorator'
2
4
 
3
5
  module Doorkeeper
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Define methods that can be called in any controller that inherits from
2
4
  # Doorkeeper::ApplicationMetalController or Doorkeeper::ApplicationController
3
5
  module Doorkeeper
@@ -53,8 +55,9 @@ module Doorkeeper
53
55
  end
54
56
 
55
57
  def enforce_content_type
56
- return if request.content_type == 'application/x-www-form-urlencoded'
57
- 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
58
61
  end
59
62
  end
60
63
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module AccessGrantMixin
3
5
  extend ActiveSupport::Concern
@@ -7,6 +9,7 @@ module Doorkeeper
7
9
  include Models::Revocable
8
10
  include Models::Accessible
9
11
  include Models::Orderable
12
+ include Models::Hashable
10
13
  include Models::Scopes
11
14
 
12
15
  # never uses pkce, if pkce migrations were not generated
@@ -28,7 +31,13 @@ module Doorkeeper
28
31
  # if there is no record with such token
29
32
  #
30
33
  def by_token(token)
31
- 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?
32
41
  end
33
42
 
34
43
  # Revokes AccessGrant records that have not been revoked and associated
@@ -42,8 +51,8 @@ module Doorkeeper
42
51
  def revoke_all_for(application_id, resource_owner, clock = Time)
43
52
  where(application_id: application_id,
44
53
  resource_owner_id: resource_owner.id,
45
- revoked_at: nil).
46
- update_all(revoked_at: clock.now.utc)
54
+ revoked_at: nil)
55
+ .update_all(revoked_at: clock.now.utc)
47
56
  end
48
57
 
49
58
  # Implements PKCE code_challenge encoding without base64 padding as described in the spec.
@@ -78,7 +87,7 @@ module Doorkeeper
78
87
  #
79
88
  # urlsafe_encode64(bin)
80
89
  # Returns the Base64-encoded version of bin. This method complies with
81
- # Base 64 Encoding with URL and Filename Safe Alphabet in RFC 4648.
90
+ # "Base 64 Encoding with URL and Filename Safe Alphabet" in RFC 4648.
82
91
  # The alphabet uses '-' instead of '+' and '_' instead of '/'.
83
92
 
84
93
  # @param code_verifier [#to_s] a one time use value (any object that responds to `#to_s`)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module AccessTokenMixin
3
5
  extend ActiveSupport::Concern
@@ -7,20 +9,21 @@ module Doorkeeper
7
9
  include Models::Revocable
8
10
  include Models::Accessible
9
11
  include Models::Orderable
12
+ include Models::Hashable
10
13
  include Models::Scopes
11
14
 
12
15
  module ClassMethods
13
16
  # Returns an instance of the Doorkeeper::AccessToken with
14
- # specific token value.
17
+ # specific plain text token value.
15
18
  #
16
19
  # @param token [#to_s]
17
- # token value (any object that responds to `#to_s`)
20
+ # Plain text token value (any object that responds to `#to_s`)
18
21
  #
19
22
  # @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
20
23
  # if there is no record with such token
21
24
  #
22
25
  def by_token(token)
23
- find_by(token: token.to_s)
26
+ find_by_plaintext_token(:token, token)
24
27
  end
25
28
 
26
29
  # Returns an instance of the Doorkeeper::AccessToken
@@ -33,7 +36,13 @@ module Doorkeeper
33
36
  # if there is no record with such refresh token
34
37
  #
35
38
  def by_refresh_token(refresh_token)
36
- 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?
37
46
  end
38
47
 
39
48
  # Revokes AccessToken records that have not been revoked and associated
@@ -47,11 +56,11 @@ module Doorkeeper
47
56
  def revoke_all_for(application_id, resource_owner, clock = Time)
48
57
  where(application_id: application_id,
49
58
  resource_owner_id: resource_owner.id,
50
- revoked_at: nil).
51
- update_all(revoked_at: clock.now.utc)
59
+ revoked_at: nil)
60
+ .update_all(revoked_at: clock.now.utc)
52
61
  end
53
62
 
54
- # Looking for not expired Access Token with a matching set of scopes
63
+ # Looking for not revoked Access Token with a matching set of scopes
55
64
  # that belongs to specific Application and Resource Owner.
56
65
  #
57
66
  # @param application [Doorkeeper::Application]
@@ -96,9 +105,9 @@ module Doorkeeper
96
105
 
97
106
  (token_scopes.sort == param_scopes.sort) &&
98
107
  Doorkeeper::OAuth::Helpers::ScopeChecker.valid?(
99
- param_scopes.to_s,
100
- Doorkeeper.configuration.scopes,
101
- app_scopes
108
+ scope_str: param_scopes.to_s,
109
+ server_scopes: Doorkeeper.configuration.scopes,
110
+ app_scopes: app_scopes
102
111
  )
103
112
  end
104
113
 
@@ -217,6 +226,26 @@ module Doorkeeper
217
226
  accessible? && includes_scope?(*scopes)
218
227
  end
219
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
+
220
249
  private
221
250
 
222
251
  # Generates refresh token with UniqueToken generator.
@@ -224,7 +253,8 @@ module Doorkeeper
224
253
  # @return [String] refresh token value
225
254
  #
226
255
  def generate_refresh_token
227
- self.refresh_token = UniqueToken.generate
256
+ @raw_refresh_token = UniqueToken.generate
257
+ self.refresh_token = hashed_or_plain_token(@raw_refresh_token)
228
258
  end
229
259
 
230
260
  # Generates and sets the token value with the
@@ -240,13 +270,16 @@ module Doorkeeper
240
270
  def generate_token
241
271
  self.created_at ||= Time.now.utc
242
272
 
243
- self.token = token_generator.generate(
273
+ @raw_token = token_generator.generate(
244
274
  resource_owner_id: resource_owner_id,
245
275
  scopes: scopes,
246
276
  application: application,
247
277
  expires_in: expires_in,
248
278
  created_at: created_at
249
279
  )
280
+
281
+ self.token = hashed_or_plain_token(@raw_token)
282
+ @raw_token
250
283
  end
251
284
 
252
285
  def token_generator
@@ -1,12 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bcrypt'
4
+
1
5
  module Doorkeeper
2
6
  module ApplicationMixin
3
7
  extend ActiveSupport::Concern
4
8
 
5
9
  include OAuth::Helpers
6
10
  include Models::Orderable
11
+ include Models::Hashable
7
12
  include Models::Scopes
8
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
9
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
+
10
38
  # Returns an instance of the Doorkeeper::Application with
11
39
  # specific UID and secret.
12
40
  #
@@ -23,7 +51,7 @@ module Doorkeeper
23
51
  app = by_uid(uid)
24
52
  return unless app
25
53
  return app if secret.blank? && !app.confidential?
26
- return unless app.secret == secret
54
+ return unless app.secret_matches?(secret)
27
55
  app
28
56
  end
29
57
 
@@ -47,5 +75,30 @@ module Doorkeeper
47
75
  def redirect_uri=(uris)
48
76
  super(uris.is_a?(Array) ? uris.join("\n") : uris)
49
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
50
103
  end
51
104
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module Models
3
5
  module Accessible
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module Models
3
5
  module Expirable
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module Models
3
5
  module Orderable
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module Models
3
5
  module Ownership
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module Models
3
5
  module Revocable
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module Models
3
5
  module Scopes
4
6
  def scopes
5
- OAuth::Scopes.from_string(self[:scopes])
7
+ OAuth::Scopes.from_string(scopes_string)
6
8
  end
7
9
 
8
10
  def scopes_string
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module OAuth
3
5
  module Authorization
@@ -14,7 +16,7 @@ module Doorkeeper
14
16
  end
15
17
 
16
18
  def native_redirect
17
- { action: :show, code: token.token }
19
+ { action: :show, code: token.plaintext_token }
18
20
  end
19
21
 
20
22
  def configuration