doorkeeper 5.5.4 → 5.8.1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -8
  3. data/README.md +5 -9
  4. data/app/controllers/doorkeeper/authorizations_controller.rb +34 -11
  5. data/app/controllers/doorkeeper/tokens_controller.rb +28 -6
  6. data/app/views/doorkeeper/authorizations/error.html.erb +3 -1
  7. data/app/views/doorkeeper/authorizations/form_post.html.erb +1 -1
  8. data/app/views/doorkeeper/authorizations/new.html.erb +16 -16
  9. data/config/locales/en.yml +4 -1
  10. data/lib/doorkeeper/config/abstract_builder.rb +1 -1
  11. data/lib/doorkeeper/config/validations.rb +15 -3
  12. data/lib/doorkeeper/config.rb +95 -55
  13. data/lib/doorkeeper/engine.rb +10 -3
  14. data/lib/doorkeeper/errors.rb +32 -0
  15. data/lib/doorkeeper/helpers/controller.rb +1 -1
  16. data/lib/doorkeeper/models/access_token_mixin.rb +71 -9
  17. data/lib/doorkeeper/models/concerns/expiration_time_sql_math.rb +88 -0
  18. data/lib/doorkeeper/models/concerns/polymorphic_resource_owner.rb +30 -0
  19. data/lib/doorkeeper/oauth/authorization/code.rb +7 -1
  20. data/lib/doorkeeper/oauth/authorization/token.rb +7 -1
  21. data/lib/doorkeeper/oauth/authorization_code_request.rb +36 -12
  22. data/lib/doorkeeper/oauth/base_request.rb +14 -12
  23. data/lib/doorkeeper/oauth/client.rb +1 -1
  24. data/lib/doorkeeper/oauth/client_credentials/creator.rb +13 -13
  25. data/lib/doorkeeper/oauth/client_credentials/issuer.rb +5 -4
  26. data/lib/doorkeeper/oauth/client_credentials/validator.rb +4 -5
  27. data/lib/doorkeeper/oauth/client_credentials_request.rb +10 -2
  28. data/lib/doorkeeper/oauth/code_request.rb +1 -1
  29. data/lib/doorkeeper/oauth/error.rb +4 -3
  30. data/lib/doorkeeper/oauth/error_response.rb +19 -4
  31. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +4 -4
  32. data/lib/doorkeeper/oauth/invalid_request_response.rb +4 -0
  33. data/lib/doorkeeper/oauth/password_access_token_request.rb +6 -6
  34. data/lib/doorkeeper/oauth/pre_authorization.rb +31 -23
  35. data/lib/doorkeeper/oauth/refresh_token_request.rb +17 -9
  36. data/lib/doorkeeper/oauth/scopes.rb +55 -1
  37. data/lib/doorkeeper/oauth/token_introspection.rb +34 -20
  38. data/lib/doorkeeper/oauth/token_request.rb +1 -1
  39. data/lib/doorkeeper/oauth/token_response.rb +5 -3
  40. data/lib/doorkeeper/orm/active_record/mixins/access_grant.rb +0 -6
  41. data/lib/doorkeeper/orm/active_record/mixins/access_token.rb +21 -4
  42. data/lib/doorkeeper/orm/active_record/mixins/application.rb +22 -4
  43. data/lib/doorkeeper/orm/active_record/redirect_uri_validator.rb +2 -2
  44. data/lib/doorkeeper/orm/active_record/stale_records_cleaner.rb +5 -2
  45. data/lib/doorkeeper/orm/active_record.rb +30 -37
  46. data/lib/doorkeeper/rails/routes.rb +12 -3
  47. data/lib/doorkeeper/rake/setup.rake +0 -5
  48. data/lib/doorkeeper/revocable_tokens/revocable_access_token.rb +21 -0
  49. data/lib/doorkeeper/revocable_tokens/revocable_refresh_token.rb +21 -0
  50. data/lib/doorkeeper/version.rb +2 -2
  51. data/lib/doorkeeper.rb +78 -5
  52. data/lib/generators/doorkeeper/remove_applications_secret_not_null_constraint_generator.rb +33 -0
  53. data/lib/generators/doorkeeper/templates/initializer.rb +44 -6
  54. data/lib/generators/doorkeeper/templates/migration.rb.erb +15 -4
  55. data/lib/generators/doorkeeper/templates/remove_applications_secret_not_null_constraint.rb.erb +7 -0
  56. metadata +28 -21
@@ -5,59 +5,11 @@ require "doorkeeper/config/option"
5
5
  require "doorkeeper/config/validations"
6
6
 
7
7
  module Doorkeeper
8
- # Defines a MissingConfiguration error for a missing Doorkeeper configuration
9
- #
10
- class MissingConfiguration < StandardError
11
- def initialize
12
- super("Configuration for doorkeeper missing. Do you have doorkeeper initializer?")
13
- end
14
- end
15
-
16
8
  # Doorkeeper option DSL could be reused in extensions to build their own
17
9
  # configurations. To use the Option DSL gems need to define `builder_class` method
18
10
  # that returns configuration Builder class. This exception raises when they don't
19
11
  # define it.
20
12
  #
21
- class MissingConfigurationBuilderClass < StandardError; end
22
-
23
- class << self
24
- def configure(&block)
25
- @config = Config::Builder.new(&block).build
26
- setup_orm_adapter
27
- setup_orm_models
28
- setup_application_owner if @config.enable_application_owner?
29
- @config
30
- end
31
-
32
- # @return [Doorkeeper::Config] configuration instance
33
- #
34
- def configuration
35
- @config || (raise MissingConfiguration)
36
- end
37
-
38
- alias config configuration
39
-
40
- def setup_orm_adapter
41
- @orm_adapter = "doorkeeper/orm/#{configuration.orm}".classify.constantize
42
- rescue NameError => e
43
- raise e, "ORM adapter not found (#{configuration.orm})", <<-ERROR_MSG.strip_heredoc
44
- [DOORKEEPER] ORM adapter not found (#{configuration.orm}), or there was an error
45
- trying to load it.
46
-
47
- You probably need to add the related gem for this adapter to work with
48
- doorkeeper.
49
- ERROR_MSG
50
- end
51
-
52
- def setup_orm_models
53
- @orm_adapter.initialize_models!
54
- end
55
-
56
- def setup_application_owner
57
- @orm_adapter.initialize_application_owner!
58
- end
59
- end
60
-
61
13
  class Config
62
14
  # Default Doorkeeper configuration builder
63
15
  class Builder < AbstractBuilder
@@ -79,6 +31,16 @@ module Doorkeeper
79
31
  @config.instance_variable_set(:@confirm_application_owner, true)
80
32
  end
81
33
 
34
+ # Provide support for dynamic scopes (e.g. user:*) (disabled by default)
35
+ # Optional parameter delimiter (default ":") if you want to customize
36
+ # the delimiter separating the scope name and matching value.
37
+ #
38
+ # @param opts [Hash] the options to configure dynamic scopes
39
+ def enable_dynamic_scopes(opts = {})
40
+ @config.instance_variable_set(:@enable_dynamic_scopes, true)
41
+ @config.instance_variable_set(:@dynamic_scopes_delimiter, opts[:delimiter] || ':')
42
+ end
43
+
82
44
  # Define default access token scopes for your provider
83
45
  #
84
46
  # @param scopes [Array] Default set of access (OAuth::Scopes.new)
@@ -137,6 +99,15 @@ module Doorkeeper
137
99
  @config.instance_variable_set(:@reuse_access_token, true)
138
100
  end
139
101
 
102
+ # Choose to use the url path for native autorization codes
103
+ # Enabling this flag sets the authorization code response route for
104
+ # native redirect uris to oauth/authorize/<code>. The default is
105
+ # oauth/authorize/native?code=<code>.
106
+ # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1143
107
+ def use_url_path_for_native_authorization
108
+ @config.instance_variable_set(:@use_url_path_for_native_authorization, true)
109
+ end
110
+
140
111
  # TODO: maybe make it more generic for other flows too?
141
112
  # Only allow one valid access token obtained via client credentials
142
113
  # per client. If a new access token is obtained before the old one
@@ -145,6 +116,19 @@ module Doorkeeper
145
116
  @config.instance_variable_set(:@revoke_previous_client_credentials_token, true)
146
117
  end
147
118
 
119
+ # Only allow one valid access token obtained via authorization code
120
+ # per client. If a new access token is obtained before the old one
121
+ # expired, the old one gets revoked (disabled by default)
122
+ def revoke_previous_authorization_code_token
123
+ @config.instance_variable_set(:@revoke_previous_authorization_code_token, true)
124
+ end
125
+
126
+ # Require non-confidential apps to use PKCE (send a code_verifier) when requesting
127
+ # an access_token using an authorization code (disabled by default)
128
+ def force_pkce
129
+ @config.instance_variable_set(:@force_pkce, true)
130
+ end
131
+
148
132
  # Use an API mode for applications generated with --api argument
149
133
  # It will skip applications controller, disable forgery protection
150
134
  def api_only
@@ -269,6 +253,7 @@ module Doorkeeper
269
253
  option :orm, default: :active_record
270
254
  option :native_redirect_uri, default: "urn:ietf:wg:oauth:2.0:oob", deprecated: true
271
255
  option :grant_flows, default: %w[authorization_code client_credentials]
256
+ option :pkce_code_challenge_methods, default: %w[plain S256]
272
257
  option :handle_auth_errors, default: :render
273
258
  option :token_lookup_batch_size, default: 10_000
274
259
  # Sets the token_reuse_limit
@@ -293,10 +278,6 @@ module Doorkeeper
293
278
  option :skip_client_authentication_for_password_grant,
294
279
  default: false
295
280
 
296
- option :active_record_options,
297
- default: {},
298
- deprecated: { message: "Customize Doorkeeper models instead" }
299
-
300
281
  # Hook to allow arbitrary user-client authorization
301
282
  option :authorize_resource_owner_for_client,
302
283
  default: ->(_client, _resource_owner) { true }
@@ -364,11 +345,29 @@ module Doorkeeper
364
345
  option :access_token_generator,
365
346
  default: "Doorkeeper::OAuth::Helpers::UniqueToken"
366
347
 
348
+ # Allows additional data to be received when granting access to an Application, and for this
349
+ # additional data to be sent with subsequently generated access tokens. The access grant and
350
+ # access token models will both need to respond to the specified attribute names.
351
+ #
352
+ # @param attributes [Array] The array of custom attribute names to be saved
353
+ #
354
+ option :custom_access_token_attributes,
355
+ default: []
356
+
357
+ # Use a custom class for generating the application secret.
358
+ # https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-application-secret-generator
359
+ #
360
+ # @param application_secret_generator [String]
361
+ # the name of the application secret generator class
362
+ #
363
+ option :application_secret_generator,
364
+ default: "Doorkeeper::OAuth::Helpers::UniqueToken"
365
+
367
366
  # Default access token generator is a SecureRandom class from Ruby stdlib.
368
367
  # This option defines which method will be used to generate a unique token value.
369
368
  #
370
- # @param access_token_generator [String]
371
- # the name of the access token generator class
369
+ # @param default_generator_method [Symbol]
370
+ # the method name of the default access token generator
372
371
  #
373
372
  option :default_generator_method, default: :urlsafe_base64
374
373
 
@@ -430,7 +429,7 @@ module Doorkeeper
430
429
  default: (lambda do |token, authorized_client, authorized_token|
431
430
  if authorized_token
432
431
  authorized_token.application == token&.application
433
- elsif token.application
432
+ elsif token&.application
434
433
  authorized_client == token.application
435
434
  else
436
435
  true
@@ -441,6 +440,16 @@ module Doorkeeper
441
440
  :token_secret_fallback_strategy,
442
441
  :application_secret_fallback_strategy
443
442
 
443
+ def clear_cache!
444
+ %i[
445
+ application_model
446
+ access_token_model
447
+ access_grant_model
448
+ ].each do |var|
449
+ remove_instance_variable("@#{var}") if instance_variable_defined?("@#{var}")
450
+ end
451
+ end
452
+
444
453
  # Doorkeeper Access Token model class.
445
454
  #
446
455
  # @return [ActiveRecord::Base, Mongoid::Document, Sequel::Model]
@@ -496,6 +505,14 @@ module Doorkeeper
496
505
  option_set? :revoke_previous_client_credentials_token
497
506
  end
498
507
 
508
+ def revoke_previous_authorization_code_token?
509
+ option_set? :revoke_previous_authorization_code_token
510
+ end
511
+
512
+ def force_pkce?
513
+ option_set? :force_pkce
514
+ end
515
+
499
516
  def enforce_configured_scopes?
500
517
  option_set? :enforce_configured_scopes
501
518
  end
@@ -504,6 +521,14 @@ module Doorkeeper
504
521
  option_set? :enable_application_owner
505
522
  end
506
523
 
524
+ def enable_dynamic_scopes?
525
+ option_set? :enable_dynamic_scopes
526
+ end
527
+
528
+ def dynamic_scopes_delimiter
529
+ @dynamic_scopes_delimiter
530
+ end
531
+
507
532
  def polymorphic_resource_owner?
508
533
  option_set? :polymorphic_resource_owner
509
534
  end
@@ -516,6 +541,10 @@ module Doorkeeper
516
541
  handle_auth_errors == :raise
517
542
  end
518
543
 
544
+ def redirect_on_errors?
545
+ handle_auth_errors == :redirect
546
+ end
547
+
519
548
  def application_secret_hashed?
520
549
  instance_variable_defined?(:"@application_secret_strategy")
521
550
  end
@@ -544,6 +573,12 @@ module Doorkeeper
544
573
  @scopes_by_grant_type ||= {}
545
574
  end
546
575
 
576
+ def pkce_code_challenge_methods_supported
577
+ return [] unless access_grant_model.pkce_supported?
578
+
579
+ pkce_code_challenge_methods
580
+ end
581
+
547
582
  def client_credentials_methods
548
583
  @client_credentials_methods ||= %i[from_basic from_params]
549
584
  end
@@ -581,6 +616,11 @@ module Doorkeeper
581
616
  def deprecated_token_grant_types_resolver
582
617
  @deprecated_token_grant_types ||= calculate_token_grant_types
583
618
  end
619
+
620
+ def native_authorization_code_route
621
+ @use_url_path_for_native_authorization = false unless defined?(@use_url_path_for_native_authorization)
622
+ @use_url_path_for_native_authorization ? '/:code' : '/native'
623
+ end
584
624
 
585
625
  # [NOTE]: deprecated and will be removed soon
586
626
  def deprecated_authorization_flows
@@ -2,9 +2,12 @@
2
2
 
3
3
  module Doorkeeper
4
4
  class Engine < Rails::Engine
5
- initializer "doorkeeper.params.filter" do |app|
6
- parameters = %w[client_secret code authentication_token access_token refresh_token]
7
- app.config.filter_parameters << /^(#{Regexp.union(parameters)})$/
5
+ initializer "doorkeeper.params.filter", after: :load_config_initializers do |app|
6
+ if Doorkeeper.configured?
7
+ parameters = %w[client_secret authentication_token access_token refresh_token]
8
+ parameters << "code" if Doorkeeper.config.grant_flows.include?("authorization_code")
9
+ app.config.filter_parameters << /^(#{Regexp.union(parameters)})$/
10
+ end
8
11
  end
9
12
 
10
13
  initializer "doorkeeper.routes" do
@@ -17,6 +20,10 @@ module Doorkeeper
17
20
  end
18
21
  end
19
22
 
23
+ config.to_prepare do
24
+ Doorkeeper.run_orm_hooks
25
+ end
26
+
20
27
  if defined?(Sprockets) && Sprockets::VERSION.chr.to_i >= 4
21
28
  initializer "doorkeeper.assets.precompile" do |app|
22
29
  # Force users to use:
@@ -6,6 +6,10 @@ module Doorkeeper
6
6
  def type
7
7
  message
8
8
  end
9
+
10
+ def self.translate_options
11
+ {}
12
+ end
9
13
  end
10
14
 
11
15
  class InvalidGrantReuse < DoorkeeperError
@@ -39,13 +43,41 @@ module Doorkeeper
39
43
  def initialize(response)
40
44
  @response = response
41
45
  end
46
+
47
+ def self.name_for_response
48
+ self.name.demodulize.underscore.to_sym
49
+ end
50
+ end
51
+
52
+ class InvalidCodeChallengeMethod < BaseResponseError
53
+ def self.translate_options
54
+ challenge_methods = Doorkeeper.config.pkce_code_challenge_methods_supported
55
+ {
56
+ challenge_methods: challenge_methods.join(", "),
57
+ count: challenge_methods.length
58
+ }
59
+ end
42
60
  end
43
61
 
44
62
  UnableToGenerateToken = Class.new(DoorkeeperError)
45
63
  TokenGeneratorNotFound = Class.new(DoorkeeperError)
46
64
  NoOrmCleaner = Class.new(DoorkeeperError)
47
65
 
66
+ InvalidRequest = Class.new(BaseResponseError)
48
67
  InvalidToken = Class.new(BaseResponseError)
68
+ InvalidClient = Class.new(BaseResponseError)
69
+ InvalidScope = Class.new(BaseResponseError)
70
+ InvalidRedirectUri = Class.new(BaseResponseError)
71
+ InvalidCodeChallenge = Class.new(BaseResponseError)
72
+ InvalidGrant = Class.new(BaseResponseError)
73
+
74
+ UnauthorizedClient = Class.new(BaseResponseError)
75
+ UnsupportedResponseType = Class.new(BaseResponseError)
76
+ UnsupportedResponseMode = Class.new(BaseResponseError)
77
+
78
+ AccessDenied = Class.new(BaseResponseError)
79
+ ServerError = Class.new(BaseResponseError)
80
+
49
81
  TokenExpired = Class.new(InvalidToken)
50
82
  TokenRevoked = Class.new(InvalidToken)
51
83
  TokenUnknown = Class.new(InvalidToken)
@@ -82,7 +82,7 @@ module Doorkeeper
82
82
  end
83
83
 
84
84
  def x_www_form_urlencoded?
85
- request.content_type == "application/x-www-form-urlencoded"
85
+ request.media_type == "application/x-www-form-urlencoded"
86
86
  end
87
87
  end
88
88
  end
@@ -13,6 +13,7 @@ module Doorkeeper
13
13
  include Models::SecretStorable
14
14
  include Models::Scopes
15
15
  include Models::ResourceOwnerable
16
+ include Models::ExpirationTimeSqlMath
16
17
 
17
18
  module ClassMethods
18
19
  # Returns an instance of the Doorkeeper::AccessToken with
@@ -82,13 +83,18 @@ module Doorkeeper
82
83
  # Resource Owner model instance or it's ID
83
84
  # @param scopes [String, Doorkeeper::OAuth::Scopes]
84
85
  # set of scopes
86
+ # @param custom_attributes [Nilable Hash]
87
+ # A nil value, or hash with keys corresponding to the custom attributes
88
+ # configured with the `custom_access_token_attributes` config option.
89
+ # A nil value will ignore custom attributes.
85
90
  #
86
91
  # @return [Doorkeeper::AccessToken, nil] Access Token instance or
87
92
  # nil if matching record was not found
88
93
  #
89
- def matching_token_for(application, resource_owner, scopes)
94
+ def matching_token_for(application, resource_owner, scopes, custom_attributes: nil, include_expired: true)
90
95
  tokens = authorized_tokens_for(application&.id, resource_owner)
91
- find_matching_token(tokens, application, scopes)
96
+ tokens = tokens.not_expired unless include_expired
97
+ find_matching_token(tokens, application, custom_attributes, scopes)
92
98
  end
93
99
 
94
100
  # Interface to enumerate access token records in batches in order not
@@ -104,9 +110,7 @@ module Doorkeeper
104
110
  #
105
111
  # ActiveRecord 5.x - 6.x ignores custom ordering so we can't perform a
106
112
  # database sort by created_at, so we need to load all the matching records,
107
- # sort them and find latest one. Probably it would be better to rewrite this
108
- # query using Time math if possible, but we n eed to consider ORM and
109
- # different databases support.
113
+ # sort them and find latest one.
110
114
  #
111
115
  # @param relation [ActiveRecord::Relation]
112
116
  # Access tokens relation
@@ -114,11 +118,15 @@ module Doorkeeper
114
118
  # Application instance
115
119
  # @param scopes [String, Doorkeeper::OAuth::Scopes]
116
120
  # set of scopes
121
+ # @param custom_attributes [Nilable Hash]
122
+ # A nil value, or hash with keys corresponding to the custom attributes
123
+ # configured with the `custom_access_token_attributes` config option.
124
+ # A nil value will ignore custom attributes.
117
125
  #
118
126
  # @return [Doorkeeper::AccessToken, nil] Access Token instance or
119
127
  # nil if matching record was not found
120
128
  #
121
- def find_matching_token(relation, application, scopes)
129
+ def find_matching_token(relation, application, custom_attributes, scopes)
122
130
  return nil unless relation
123
131
 
124
132
  matching_tokens = []
@@ -126,7 +134,8 @@ module Doorkeeper
126
134
 
127
135
  find_access_token_in_batches(relation, batch_size: batch_size) do |batch|
128
136
  tokens = batch.select do |token|
129
- scopes_match?(token.scopes, scopes, application&.scopes)
137
+ scopes_match?(token.scopes, scopes, application&.scopes) &&
138
+ custom_attributes_match?(token, custom_attributes)
130
139
  end
131
140
 
132
141
  matching_tokens.concat(tokens)
@@ -160,6 +169,31 @@ module Doorkeeper
160
169
  )
161
170
  end
162
171
 
172
+ # Checks whether the token custom attribute values match the custom
173
+ # attributes from the parameters.
174
+ #
175
+ # @param token [Doorkeeper::AccessToken]
176
+ # The access token whose custom attributes are being compared
177
+ # to the custom_attributes.
178
+ #
179
+ # @param custom_attributes [Hash]
180
+ # A hash of the attributes for which we want to determine whether
181
+ # the token's custom attributes match.
182
+ #
183
+ # @return [Boolean] true if the token's custom attribute values
184
+ # match those in the custom_attributes, or if both are empty/blank.
185
+ # False otherwise.
186
+ def custom_attributes_match?(token, custom_attributes)
187
+ return true if custom_attributes.nil?
188
+
189
+ token_attribs = token.custom_attributes
190
+ return true if token_attribs.blank? && custom_attributes.blank?
191
+
192
+ Doorkeeper.config.custom_access_token_attributes.all? do |attribute|
193
+ token_attribs[attribute] == custom_attributes[attribute]
194
+ end
195
+ end
196
+
163
197
  # Looking for not expired AccessToken record with a matching set of
164
198
  # scopes that belongs to specific Application and Resource Owner.
165
199
  # If it doesn't exists - then creates it.
@@ -180,8 +214,12 @@ module Doorkeeper
180
214
  # @return [Doorkeeper::AccessToken] existing record or a new one
181
215
  #
182
216
  def find_or_create_for(application:, resource_owner:, scopes:, **token_attributes)
217
+ scopes = Doorkeeper::OAuth::Scopes.from_string(scopes) if scopes.is_a?(String)
218
+
183
219
  if Doorkeeper.config.reuse_access_token
184
- access_token = matching_token_for(application, resource_owner, scopes)
220
+ custom_attributes = extract_custom_attributes(token_attributes).presence
221
+ access_token = matching_token_for(
222
+ application, resource_owner, scopes, custom_attributes: custom_attributes, include_expired: false)
185
223
 
186
224
  return access_token if access_token&.reusable?
187
225
  end
@@ -213,7 +251,7 @@ module Doorkeeper
213
251
  # @return [Doorkeeper::AccessToken] new access token
214
252
  #
215
253
  def create_for(application:, resource_owner:, scopes:, **token_attributes)
216
- token_attributes[:application_id] = application&.id
254
+ token_attributes[:application] = application
217
255
  token_attributes[:scopes] = scopes.to_s
218
256
 
219
257
  if Doorkeeper.config.polymorphic_resource_owner?
@@ -276,6 +314,18 @@ module Doorkeeper
276
314
  def fallback_secret_strategy
277
315
  ::Doorkeeper.config.token_secret_fallback_strategy
278
316
  end
317
+
318
+ # Extracts the token's custom attributes (defined by the
319
+ # custom_access_token_attributes config option) from the token's attributes.
320
+ #
321
+ # @param attributes [Hash]
322
+ # A hash of the access token's attributes.
323
+ # @return [Hash]
324
+ # A hash containing only the custom access token attributes.
325
+ def extract_custom_attributes(attributes)
326
+ attributes.with_indifferent_access.slice(
327
+ *Doorkeeper.configuration.custom_access_token_attributes)
328
+ end
279
329
  end
280
330
 
281
331
  # Access Token type: Bearer.
@@ -308,6 +358,14 @@ module Doorkeeper
308
358
  end
309
359
  end
310
360
 
361
+ # The token's custom attributes, as defined by
362
+ # the custom_access_token_attributes config option.
363
+ #
364
+ # @return [Hash] hash of custom access token attributes.
365
+ def custom_attributes
366
+ self.class.extract_custom_attributes(attributes)
367
+ end
368
+
311
369
  # Indicates whether the token instance have the same credential
312
370
  # as the other Access Token.
313
371
  #
@@ -435,6 +493,10 @@ module Doorkeeper
435
493
  if Doorkeeper.config.polymorphic_resource_owner?
436
494
  attributes[:resource_owner] = resource_owner
437
495
  end
496
+
497
+ Doorkeeper.config.custom_access_token_attributes.each do |attribute_name|
498
+ attributes[attribute_name] = public_send(attribute_name)
499
+ end
438
500
  end
439
501
  end
440
502
 
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module Models
5
+ module ExpirationTimeSqlMath
6
+ extend ::ActiveSupport::Concern
7
+
8
+ class ExpirationTimeSqlGenerator
9
+ attr_reader :model
10
+
11
+ delegate :table_name, to: :@model
12
+
13
+ def initialize(model)
14
+ @model = model
15
+ end
16
+
17
+ def generate_sql
18
+ raise "`generate_sql` should be overridden for a #{self.class.name}!"
19
+ end
20
+ end
21
+
22
+ class MySqlExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator
23
+ def generate_sql
24
+ Arel.sql("DATE_ADD(#{table_name}.created_at, INTERVAL #{table_name}.expires_in SECOND)")
25
+ end
26
+ end
27
+
28
+ class SqlLiteExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator
29
+ def generate_sql
30
+ Arel.sql("DATETIME(#{table_name}.created_at, '+' || #{table_name}.expires_in || ' SECONDS')")
31
+ end
32
+ end
33
+
34
+ class SqlServerExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator
35
+ def generate_sql
36
+ Arel.sql("DATEADD(second, #{table_name}.expires_in, #{table_name}.created_at) AT TIME ZONE 'UTC'")
37
+ end
38
+ end
39
+
40
+ class OracleExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator
41
+ def generate_sql
42
+ Arel.sql("#{table_name}.created_at + INTERVAL to_char(#{table_name}.expires_in) second")
43
+ end
44
+ end
45
+
46
+ class PostgresExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator
47
+ def generate_sql
48
+ Arel.sql("#{table_name}.created_at + #{table_name}.expires_in * INTERVAL '1 SECOND'")
49
+ end
50
+ end
51
+
52
+ ADAPTERS_MAPPING = {
53
+ "sqlite" => SqlLiteExpirationTimeSqlGenerator,
54
+ "sqlite3" => SqlLiteExpirationTimeSqlGenerator,
55
+ "postgis" => PostgresExpirationTimeSqlGenerator,
56
+ "postgresql" => PostgresExpirationTimeSqlGenerator,
57
+ "mysql" => MySqlExpirationTimeSqlGenerator,
58
+ "mysql2" => MySqlExpirationTimeSqlGenerator,
59
+ "trilogy" => MySqlExpirationTimeSqlGenerator,
60
+ "sqlserver" => SqlServerExpirationTimeSqlGenerator,
61
+ "oracleenhanced" => OracleExpirationTimeSqlGenerator,
62
+ }.freeze
63
+
64
+ module ClassMethods
65
+ def supports_expiration_time_math?
66
+ ADAPTERS_MAPPING.key?(adapter_name.downcase) ||
67
+ respond_to?(:custom_expiration_time_sql)
68
+ end
69
+
70
+ def expiration_time_sql
71
+ if respond_to?(:custom_expiration_time_sql)
72
+ custom_expiration_time_sql
73
+ else
74
+ expiration_time_sql_expression
75
+ end
76
+ end
77
+
78
+ def expiration_time_sql_expression
79
+ ADAPTERS_MAPPING.fetch(adapter_name.downcase).new(self).generate_sql
80
+ end
81
+
82
+ def adapter_name
83
+ ActiveRecord::Base.connection.adapter_name
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module Models
5
+ module PolymorphicResourceOwner
6
+ module ForAccessGrant
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ if Doorkeeper.config.polymorphic_resource_owner?
11
+ belongs_to :resource_owner, polymorphic: true, optional: false
12
+ else
13
+ validates :resource_owner_id, presence: true
14
+ end
15
+ end
16
+ end
17
+
18
+ module ForAccessToken
19
+ extend ActiveSupport::Concern
20
+
21
+ included do
22
+ if Doorkeeper.config.polymorphic_resource_owner?
23
+ belongs_to :resource_owner, polymorphic: true, optional: true
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
@@ -45,7 +45,13 @@ module Doorkeeper
45
45
  attributes[:resource_owner_id] = resource_owner.id
46
46
  end
47
47
 
48
- pkce_attributes.merge(attributes)
48
+ pkce_attributes.merge(attributes).merge(custom_attributes)
49
+ end
50
+
51
+ def custom_attributes
52
+ # Custom access token attributes are saved into the access grant,
53
+ # and then included in subsequently generated access tokens.
54
+ @pre_auth.custom_access_token_attributes.to_h.with_indifferent_access
49
55
  end
50
56
 
51
57
  def pkce_attributes
@@ -60,7 +60,7 @@ module Doorkeeper
60
60
  )
61
61
 
62
62
  @token = Doorkeeper.config.access_token_model.find_or_create_for(
63
- application: pre_auth.client,
63
+ application: application,
64
64
  resource_owner: resource_owner,
65
65
  scopes: pre_auth.scopes,
66
66
  expires_in: self.class.access_token_expires_in(Doorkeeper.config, context),
@@ -68,6 +68,12 @@ module Doorkeeper
68
68
  )
69
69
  end
70
70
 
71
+ def application
72
+ return unless pre_auth.client
73
+
74
+ pre_auth.client.is_a?(Doorkeeper.config.application_model) ? pre_auth.client : pre_auth.client.application
75
+ end
76
+
71
77
  def oob_redirect
72
78
  {
73
79
  controller: controller,