doorkeeper-sequel 1.5.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +6 -0
  3. data/.gitignore +1 -0
  4. data/.gitmodules +0 -1
  5. data/.rubocop.yml +29 -2
  6. data/.travis.yml +3 -12
  7. data/CHANGELOG.md +13 -3
  8. data/Gemfile +13 -9
  9. data/README.md +18 -3
  10. data/Rakefile +22 -18
  11. data/config/locales/en.yml +11 -0
  12. data/doorkeeper-sequel.gemspec +27 -23
  13. data/gemfiles/rails-5.0.gemfile +9 -6
  14. data/gemfiles/rails-5.1.gemfile +9 -6
  15. data/gemfiles/rails-5.2.gemfile +9 -6
  16. data/gemfiles/rails-6.0.gemfile +14 -0
  17. data/lib/doorkeeper-sequel.rb +21 -17
  18. data/lib/doorkeeper-sequel/gem_version.rb +5 -3
  19. data/lib/doorkeeper-sequel/generators/application_owner_generator.rb +5 -3
  20. data/lib/doorkeeper-sequel/generators/concerns/migration_actions.rb +8 -6
  21. data/lib/doorkeeper-sequel/generators/confidential_applications_generator.rb +5 -3
  22. data/lib/doorkeeper-sequel/generators/migration_generator.rb +5 -3
  23. data/lib/doorkeeper-sequel/generators/pkce_generator.rb +16 -0
  24. data/lib/doorkeeper-sequel/generators/polymorphic_resource_owner_generator.rb +16 -0
  25. data/lib/doorkeeper-sequel/generators/previous_refresh_token_generator.rb +5 -3
  26. data/lib/doorkeeper-sequel/generators/templates/add_confidential_to_application_migration.rb +2 -0
  27. data/lib/doorkeeper-sequel/generators/templates/add_owner_to_application.rb +3 -1
  28. data/lib/doorkeeper-sequel/generators/templates/add_previous_refresh_token_to_access_tokens.rb +3 -1
  29. data/lib/doorkeeper-sequel/generators/templates/create_doorkeeper_tables.rb +7 -3
  30. data/lib/doorkeeper-sequel/generators/templates/enable_pkce_migration.rb +10 -0
  31. data/lib/doorkeeper-sequel/generators/templates/enable_polymorphic_resource_owner_migration.rb +15 -0
  32. data/lib/doorkeeper-sequel/mixins/access_grant_mixin.rb +45 -7
  33. data/lib/doorkeeper-sequel/mixins/access_token_mixin.rb +154 -42
  34. data/lib/doorkeeper-sequel/mixins/application_mixin.rb +116 -22
  35. data/lib/doorkeeper-sequel/mixins/concerns/ownership.rb +2 -0
  36. data/lib/doorkeeper-sequel/mixins/concerns/sequel_compat.rb +33 -3
  37. data/lib/doorkeeper-sequel/railtie.rb +3 -1
  38. data/lib/doorkeeper-sequel/tasks/doorkeeper-sequel.rake +20 -3
  39. data/lib/doorkeeper-sequel/validators/redirect_uri_validator.rb +35 -6
  40. data/lib/doorkeeper-sequel/version.rb +3 -1
  41. data/lib/doorkeeper/orm/sequel.rb +11 -4
  42. data/lib/doorkeeper/orm/sequel/access_grant.rb +18 -0
  43. data/lib/doorkeeper/orm/sequel/access_token.rb +6 -0
  44. data/lib/doorkeeper/orm/sequel/application.rb +34 -3
  45. data/lib/doorkeeper/orm/sequel/stale_records_cleaner.rb +26 -0
  46. data/lib/doorkeeper/redirect_uri_validator.rb +9 -0
  47. data/spec/stubs/config/initializers/db.rb +6 -1
  48. data/spec/stubs/spec_helper_integration.rb +9 -11
  49. metadata +225 -235
  50. data/gemfiles/rails-4.2.gemfile +0 -13
@@ -1,4 +1,6 @@
1
- require_relative '../validators/redirect_uri_validator'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../validators/redirect_uri_validator"
2
4
 
3
5
  module DoorkeeperSequel
4
6
  module ApplicationMixin
@@ -7,6 +9,7 @@ module DoorkeeperSequel
7
9
  include SequelCompat
8
10
  include Doorkeeper::OAuth::Helpers
9
11
  include Doorkeeper::Models::Scopes
12
+ include Doorkeeper::Models::SecretStorable
10
13
  include DoorkeeperSequel::RedirectUriValidator
11
14
 
12
15
  included do
@@ -14,30 +17,86 @@ module DoorkeeperSequel
14
17
  plugin :timestamps
15
18
  plugin :association_dependencies
16
19
 
17
- one_to_many :access_grants, class: 'Doorkeeper::AccessGrant'
18
- one_to_many :access_tokens, class: 'Doorkeeper::AccessToken'
20
+ one_to_many :access_grants, class: "Doorkeeper::AccessGrant"
21
+ one_to_many :access_tokens, class: "Doorkeeper::AccessToken"
19
22
 
20
23
  add_association_dependencies access_grants: :delete, access_tokens: :delete
21
24
 
22
25
  set_allowed_columns :name, :redirect_uri, :scopes, :confidential
23
26
 
24
27
  def before_validation
25
- generate_uid
26
- generate_secret
28
+ if new?
29
+ generate_uid
30
+ generate_secret
31
+ end
27
32
  super
28
33
  end
29
34
 
30
35
  def validate
31
36
  super
32
- validates_presence [:name, :secret, :uid]
37
+ validates_presence %i[name secret uid]
33
38
  validates_unique [:uid]
34
39
  validates_redirect_uri :redirect_uri
35
40
  validates_includes [true, false], :confidential, allow_missing: true
36
41
 
42
+ validate_scopes_match_configured if enforce_scopes?
43
+
37
44
  if respond_to?(:validate_owner?)
38
45
  validates_presence [:owner_id] if validate_owner?
39
46
  end
40
47
  end
48
+
49
+ # In some cases, Doorkeeper used as a proxy app. In this case database does not have any fields.
50
+ # Even table may not exists on source database.
51
+ # Aliasing this method throws NoMethod Error. Due to this we need to explicitly
52
+ # define confidential? here.
53
+ def confidential?
54
+ confidential.present? && !!confidential
55
+ end
56
+
57
+ def renew_secret
58
+ @raw_secret = Doorkeeper::OAuth::Helpers::UniqueToken.generate
59
+ secret_strategy.store_secret(self, :secret, @raw_secret)
60
+ end
61
+
62
+ def as_json(options = {})
63
+ if (respond_to?(:owner) && owner && owner == options[:current_resource_owner]) ||
64
+ options[:as_owner]
65
+ hash = JSON.parse(to_json, symbolize_names: false)
66
+ else
67
+ only = extract_serializable_attributes(options)
68
+ # TODO: Write our own serializer for Hash
69
+ hash = JSON.parse(to_json(options.merge(only: only)), symbolize_names: false)
70
+ end
71
+ hash["secret"] = plaintext_secret if hash.key?("secret")
72
+ hash
73
+ end
74
+
75
+ def plaintext_secret
76
+ if secret_strategy.allows_restoring_secrets?
77
+ secret_strategy.restore_secret(self, :secret)
78
+ else
79
+ @raw_secret
80
+ end
81
+ end
82
+
83
+ def authorized_for_resource_owner?(resource_owner)
84
+ Doorkeeper.configuration.authorize_resource_owner_for_client.call(self, resource_owner)
85
+ end
86
+
87
+ protected
88
+
89
+ def validate_scopes_match_configured
90
+ if scopes.present? && !Doorkeeper::OAuth::Helpers::ScopeChecker.valid?(scope_str: scopes.to_s,
91
+ server_scopes: Doorkeeper.configuration.scopes)
92
+ scope = "sequel.errors.models.doorkeeper/application.attributes.scopes"
93
+ errors.add(:scopes, I18n.t(:not_match_configured, scope: scope))
94
+ end
95
+ end
96
+
97
+ def enforce_scopes?
98
+ Doorkeeper.configuration.enforce_configured_scopes?
99
+ end
41
100
  end
42
101
 
43
102
  module ClassMethods
@@ -45,7 +104,8 @@ module DoorkeeperSequel
45
104
  app = by_uid(uid)
46
105
  return unless app
47
106
  return app if secret.blank? && !app.confidential?
48
- return unless app.secret == secret
107
+ return unless app.secret_matches?(secret)
108
+
49
109
  app
50
110
  end
51
111
 
@@ -53,33 +113,64 @@ module DoorkeeperSequel
53
113
  first(uid: uid.to_s)
54
114
  end
55
115
 
56
- def supports_confidentiality?
57
- column_names.include?('confidential')
116
+ def find_by(params)
117
+ first(params)
58
118
  end
59
119
 
60
120
  def column_names
61
121
  columns.map(&:to_s)
62
122
  end
123
+
124
+ def secret_strategy
125
+ ::Doorkeeper.configuration.application_secret_strategy
126
+ end
127
+
128
+ def fallback_secret_strategy
129
+ ::Doorkeeper.configuration.application_secret_fallback_strategy
130
+ end
63
131
  end
64
132
 
65
- # Fallback to existing, default behaviour of assuming all apps to be
66
- # confidential if the migration hasn't been run
67
- def confidential
68
- return super if self.class.supports_confidentiality?
133
+ def secret_matches?(input)
134
+ # return false if either is nil, since secure_compare depends on strings
135
+ # but Application secrets MAY be nil depending on confidentiality.
136
+ return false if input.nil? || secret.nil?
69
137
 
70
- ActiveSupport::Deprecation.warn 'You are susceptible to security bug ' \
71
- 'CVE-2018-1000211. Please follow instructions outlined in ' \
72
- 'Doorkeeper::CVE_2018_1000211_WARNING'
138
+ # When matching the secret by comparer function, all is well.
139
+ return true if secret_strategy.secret_matches?(input, secret)
73
140
 
74
- true
141
+ # When fallback lookup is enabled, ensure applications
142
+ # with plain secrets can still be found
143
+ if fallback_secret_strategy
144
+ fallback_secret_strategy.secret_matches?(input, secret)
145
+ else
146
+ false
147
+ end
75
148
  end
76
149
 
77
- alias_method :confidential?, :confidential
78
-
79
150
  private
80
-
151
+
152
+ def extract_serializable_attributes(options = {})
153
+ opts = options.try(:dup) || {}
154
+ only = Array.wrap(opts[:only]).map(&:to_s)
155
+
156
+ only = if only.blank?
157
+ serializable_attributes
158
+ else
159
+ only & serializable_attributes
160
+ end
161
+
162
+ only -= Array.wrap(opts[:except]).map(&:to_s) if opts.key?(:except)
163
+ only.uniq
164
+ end
165
+
166
+ def serializable_attributes
167
+ attributes = %w[id name created_at]
168
+ attributes << "uid" unless confidential?
169
+ attributes
170
+ end
171
+
81
172
  def has_scopes?
82
- Doorkeeper::Application.columns.include?('scopes')
173
+ Doorkeeper::Application.columns.include?("scopes")
83
174
  end
84
175
 
85
176
  def generate_uid
@@ -87,7 +178,10 @@ module DoorkeeperSequel
87
178
  end
88
179
 
89
180
  def generate_secret
90
- self.secret = UniqueToken.generate if secret.blank? && new?
181
+ return unless secret.blank?
182
+
183
+ @raw_secret = UniqueToken.generate
184
+ secret_strategy.store_secret(self, :secret, @raw_secret)
91
185
  end
92
186
  end
93
187
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DoorkeeperSequel
2
4
  module Ownership
3
5
  extend ActiveSupport::Concern
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DoorkeeperSequel
2
4
  module SequelCompat
3
5
  extend ActiveSupport::Concern
@@ -8,9 +10,7 @@ module DoorkeeperSequel
8
10
  plugin :active_model
9
11
 
10
12
  # Sequel 4.47 and higher deprecated #set_allowed_columns
11
- if (::Sequel::MAJOR >= 4 && ::Sequel::MINOR >= 47) || ::Sequel::MAJOR >= 5
12
- plugin :whitelist_security
13
- end
13
+ plugin :whitelist_security if (::Sequel::MAJOR >= 4 && ::Sequel::MINOR >= 47) || ::Sequel::MAJOR >= 5
14
14
 
15
15
  self.raise_on_save_failure = false
16
16
 
@@ -19,6 +19,8 @@ module DoorkeeperSequel
19
19
  save(columns: [column.to_sym], validate: false)
20
20
  end
21
21
 
22
+ alias_method :update_column, :update_attribute
23
+
22
24
  def update_attributes(*args)
23
25
  update(*args)
24
26
  end
@@ -34,6 +36,26 @@ module DoorkeeperSequel
34
36
  def invalid?
35
37
  !valid?
36
38
  end
39
+
40
+ def exists?
41
+ !empty?
42
+ end
43
+
44
+ def with_lock
45
+ return yield if @_tr_is_locked
46
+ @_tr_is_locked = true
47
+
48
+ begin
49
+ db.transaction do
50
+ lock!
51
+ yield
52
+ end
53
+ ensure
54
+ @_tr_is_locked = false
55
+ end
56
+ end
57
+
58
+ alias_method :exist?, :exists?
37
59
  end
38
60
 
39
61
  module ClassMethods
@@ -58,6 +80,14 @@ module DoorkeeperSequel
58
80
  super(id: args)
59
81
  end
60
82
  end
83
+
84
+ def exists?(*args)
85
+ if args.any?
86
+ !where(*args).empty?
87
+ else
88
+ !empty?
89
+ end
90
+ end
61
91
  end
62
92
  end
63
93
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DoorkeeperSequel
2
4
  class Railtie < ::Rails::Railtie
3
5
  rake_tasks do
4
- load File.expand_path('../tasks/doorkeeper-sequel.rake', __FILE__)
6
+ load File.expand_path("tasks/doorkeeper-sequel.rake", __dir__)
5
7
  end
6
8
  end
7
9
  end
@@ -1,18 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  namespace :doorkeeper_sequel do
2
4
  namespace :generate do
3
- desc 'Generate main migration file'
5
+ desc "Generate main migration file"
4
6
  task :migration do
5
7
  DoorkeeperSequel::MigrationGenerator.start
6
8
  end
7
9
 
8
- desc 'Generate migration file for Application Owner functionality'
10
+ desc "Generate migration file for Application Owner functionality"
9
11
  task :application_owner do
10
12
  DoorkeeperSequel::ApplicationOwnerGenerator.start
11
13
  end
12
14
 
13
- desc 'Generate migration file for Previous Refresh Token functionality'
15
+ desc "Generate migration file for Previous Refresh Token functionality"
14
16
  task :previous_refresh_token do
15
17
  DoorkeeperSequel::PreviousRefreshTokenGenerator.start
16
18
  end
19
+
20
+ desc "Generate migration file for PKCE"
21
+ task :pkce do
22
+ DoorkeeperSequel::PkceGenerator.start
23
+ end
24
+
25
+ desc "Add confidential column to Doorkeeper applications"
26
+ task :confidential_applications do
27
+ DoorkeeperSequel::ConfidentialApplicationsGenerator.start
28
+ end
29
+
30
+ desc "Add resource owner type to Access tokens and grants"
31
+ task :polymorphic_resource_owner do
32
+ DoorkeeperSequel::PolymorphicResourceOwnerGenerator.start
33
+ end
17
34
  end
18
35
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DoorkeeperSequel
2
4
  module RedirectUriValidator
3
5
  extend ActiveSupport::Concern
@@ -6,12 +8,18 @@ module DoorkeeperSequel
6
8
  def validates_redirect_uri(attribute)
7
9
  value = self[attribute]
8
10
 
11
+ # Allow nil? but do not allow empty string
9
12
  if value.blank?
10
- add_error(attribute, :blank)
13
+ if Doorkeeper.configuration.allow_blank_redirect_uri?(self)
14
+ true
15
+ else
16
+ add_error(attribute, :blank)
17
+ end
11
18
  else
12
19
  value.split.each do |val|
20
+ next if oob_redirect_uri?(val)
21
+
13
22
  uri = ::URI.parse(val)
14
- next if native_redirect_uri?(uri)
15
23
  validate_uri(uri, attribute)
16
24
  end
17
25
  end
@@ -21,6 +29,10 @@ module DoorkeeperSequel
21
29
 
22
30
  private
23
31
 
32
+ def oob_redirect_uri?(uri)
33
+ ::Doorkeeper::OAuth::NonStandard::IETF_WG_OAUTH2_OOB_METHODS.include?(uri)
34
+ end
35
+
24
36
  def native_redirect_uri?(uri)
25
37
  native_redirect_uri.present? && uri.to_s == native_redirect_uri.to_s
26
38
  end
@@ -33,17 +45,34 @@ module DoorkeeperSequel
33
45
  def validate_uri(uri, attribute)
34
46
  {
35
47
  fragment_present: uri.fragment.present?,
36
- relative_uri: uri.scheme.nil? || uri.host.nil?,
37
48
  secured_uri: invalid_ssl_uri?(uri),
38
- forbidden_uri: forbidden_uri?(uri)
49
+ forbidden_uri: forbidden_uri?(uri),
50
+ unspecified_scheme: unspecified_scheme?(uri),
51
+ unspecified_host: unspecified_host?(uri),
52
+ relative_uri: relative_uri?(uri),
39
53
  }.each do |error, condition|
40
54
  add_error(attribute, error) if condition
41
55
  end
42
56
  end
43
57
 
58
+ def unspecified_scheme?(uri)
59
+ return true if uri.opaque.present?
60
+
61
+ %w[localhost].include?(uri.try(:scheme))
62
+ end
63
+
64
+
65
+ def unspecified_host?(uri)
66
+ uri.is_a?(URI::HTTP) && uri.host.nil?
67
+ end
68
+
69
+ def relative_uri?(uri)
70
+ uri.scheme.nil? && uri.host.nil?
71
+ end
72
+
44
73
  def invalid_ssl_uri?(uri)
45
74
  forces_ssl = Doorkeeper.configuration.force_ssl_in_redirect_uri
46
- non_https = uri.try(:scheme) == 'http'
75
+ non_https = uri.try(:scheme) == "http"
47
76
 
48
77
  if forces_ssl.respond_to?(:call)
49
78
  forces_ssl.call(uri) && non_https
@@ -57,7 +86,7 @@ module DoorkeeperSequel
57
86
  end
58
87
 
59
88
  def add_error(attribute, error)
60
- scope = 'sequel.errors.models.doorkeeper/application.attributes.redirect_uri'
89
+ scope = "sequel.errors.models.doorkeeper/application.attributes.redirect_uri"
61
90
  errors.add(attribute, I18n.t(error, scope: scope))
62
91
  end
63
92
  end
@@ -1,4 +1,6 @@
1
- require_relative 'gem_version'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gem_version"
2
4
 
3
5
  module DoorkeeperSequel
4
6
  def self.version
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "doorkeeper/orm/sequel/stale_records_cleaner"
4
+
1
5
  module Doorkeeper
2
6
  module Orm
3
7
  module Sequel
@@ -7,18 +11,21 @@ module Doorkeeper
7
11
  # all the rake tasks (db:create, db:migrate, etc) would be aborted due to error.
8
12
  old_value = ::Sequel::Model.require_valid_table
9
13
  ::Sequel::Model.require_valid_table = false
14
+ ::Sequel::Model.strict_param_setting = false
15
+ ::Sequel::Model.plugin :json_serializer
16
+ ::Sequel::Model.plugin :polymorphic
10
17
 
11
18
  begin
12
- require 'doorkeeper/orm/sequel/access_grant'
13
- require 'doorkeeper/orm/sequel/access_token'
14
- require 'doorkeeper/orm/sequel/application'
19
+ require "doorkeeper/orm/sequel/access_grant"
20
+ require "doorkeeper/orm/sequel/access_token"
21
+ require "doorkeeper/orm/sequel/application"
15
22
  ensure
16
23
  ::Sequel::Model.require_valid_table = old_value
17
24
  end
18
25
  end
19
26
 
20
27
  def self.initialize_application_owner!
21
- require 'doorkeeper-sequel/mixins/concerns/ownership'
28
+ require "doorkeeper-sequel/mixins/concerns/ownership"
22
29
 
23
30
  Doorkeeper::Application.send :include, DoorkeeperSequel::Ownership
24
31
  end