doorkeeper 5.7.1 → 5.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df8ee24bf06e6b24c9ee822c24abf45ce0424b93ff05361dcdff76c930fa3c5a
4
- data.tar.gz: 56e84b30480a60d02eea4b417f41c1cd6b322365bfce0e9fa31aad504def3807
3
+ metadata.gz: f4907be020648eb5c1dbc6e596c31c72c4133740e849ee1345ff337a4ce01c56
4
+ data.tar.gz: 831e491b48efe921e43065d393c6a785a9988e6057f1ed3e1e4f2e8626555e65
5
5
  SHA512:
6
- metadata.gz: d25945505890cb67e1e2db1e0a7eb8d49cd8bcccf05d2732883df6ace7269fe4bbe3de491a563ea3e254e1ac16e2daa407f665078cf243f7131a26db00b39842
7
- data.tar.gz: 2269f220720be56f31928a1ab4180254058bc2b636994160db72030bc32f0a5db695e76b3186f6b0c24b8d77565bd4c49911d4f5a632b14c6bd57e213c278694
6
+ metadata.gz: 2f97a8c5d6749cea33b31737988b26271d1ad03f4bd8f003b3674e8e81159a44e6a54f5611b868a06a9a074995bcc47165f222aadcd961746cc43776ee993c57
7
+ data.tar.gz: 3d2e13efa8bba0cedc4a63055d6ecb70f0af8d1dc0644047b4b1ecb3bc3bf5affd2a0a20d46989a4999b4361573c39ae72f516398c67ffe708512f0fd6e23011
data/CHANGELOG.md CHANGED
@@ -9,6 +9,16 @@ User-visible changes worth mentioning.
9
9
 
10
10
  Add your entry here.
11
11
 
12
+ ## 5.8.0
13
+
14
+ - [#1739] Add support for dynamic scopes
15
+ - [#1715] Fix token introspection invalid request reason
16
+ - [#1714] Fix `Doorkeeper::AccessToken.find_or_create_for` with empty scopes which raises NoMethodError
17
+ - [#1712] Add `Pragma: no-cache` to token response
18
+ - [#1726] Refactor token introspection class.
19
+ - [#1727] Allow to set null secret value for Applications if they are public.
20
+ - [#1735] Add `pkce_code_challenge_methods` config option.
21
+
12
22
  ## 5.7.1
13
23
 
14
24
  - [#1705] Add `force_pkce` option that requires non-confidential clients to use PKCE when requesting an access_token using an authorization code
@@ -11,6 +11,7 @@ module Doorkeeper
11
11
  validate_reuse_access_token_value
12
12
  validate_token_reuse_limit
13
13
  validate_secret_strategies
14
+ validate_pkce_code_challenge_methods
14
15
  end
15
16
 
16
17
  private
@@ -48,6 +49,17 @@ module Doorkeeper
48
49
  )
49
50
  @token_reuse_limit = 100
50
51
  end
52
+
53
+ def validate_pkce_code_challenge_methods
54
+ return if pkce_code_challenge_methods.all? {|method| method =~ /^plain$|^S256$/ }
55
+
56
+ ::Rails.logger.warn(
57
+ "[DOORKEEPER] You have configured an invalid value for pkce_code_challenge_methods option. " \
58
+ "It will be set to default ['plain', 'S256']",
59
+ )
60
+
61
+ @pkce_code_challenge_methods = ['plain', 'S256']
62
+ end
51
63
  end
52
64
  end
53
65
  end
@@ -31,6 +31,16 @@ module Doorkeeper
31
31
  @config.instance_variable_set(:@confirm_application_owner, true)
32
32
  end
33
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
+
34
44
  # Define default access token scopes for your provider
35
45
  #
36
46
  # @param scopes [Array] Default set of access (OAuth::Scopes.new)
@@ -243,6 +253,7 @@ module Doorkeeper
243
253
  option :orm, default: :active_record
244
254
  option :native_redirect_uri, default: "urn:ietf:wg:oauth:2.0:oob", deprecated: true
245
255
  option :grant_flows, default: %w[authorization_code client_credentials]
256
+ option :pkce_code_challenge_methods, default: %w[plain S256]
246
257
  option :handle_auth_errors, default: :render
247
258
  option :token_lookup_batch_size, default: 10_000
248
259
  # Sets the token_reuse_limit
@@ -418,7 +429,7 @@ module Doorkeeper
418
429
  default: (lambda do |token, authorized_client, authorized_token|
419
430
  if authorized_token
420
431
  authorized_token.application == token&.application
421
- elsif token.application
432
+ elsif token&.application
422
433
  authorized_client == token.application
423
434
  else
424
435
  true
@@ -510,6 +521,14 @@ module Doorkeeper
510
521
  option_set? :enable_application_owner
511
522
  end
512
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
+
513
532
  def polymorphic_resource_owner?
514
533
  option_set? :polymorphic_resource_owner
515
534
  end
@@ -554,6 +573,12 @@ module Doorkeeper
554
573
  @scopes_by_grant_type ||= {}
555
574
  end
556
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
+
557
582
  def client_credentials_methods
558
583
  @client_credentials_methods ||= %i[from_basic from_params]
559
584
  end
@@ -214,6 +214,8 @@ module Doorkeeper
214
214
  # @return [Doorkeeper::AccessToken] existing record or a new one
215
215
  #
216
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
+
217
219
  if Doorkeeper.config.reuse_access_token
218
220
  custom_attributes = extract_custom_attributes(token_attributes).presence
219
221
  access_token = matching_token_for(
@@ -154,7 +154,7 @@ module Doorkeeper
154
154
  return true unless Doorkeeper.config.access_grant_model.pkce_supported?
155
155
 
156
156
  code_challenge.blank? ||
157
- (code_challenge_method.present? && code_challenge_method =~ /^plain$|^S256$/)
157
+ (code_challenge_method.present? && Doorkeeper.config.pkce_code_challenge_methods_supported.include?(code_challenge_method))
158
158
  end
159
159
 
160
160
  def response_on_fragment?
@@ -6,6 +6,8 @@ module Doorkeeper
6
6
  include Enumerable
7
7
  include Comparable
8
8
 
9
+ DYNAMIC_SCOPE_WILDCARD = "*"
10
+
9
11
  def self.from_string(string)
10
12
  string ||= ""
11
13
  new.tap do |scope|
@@ -26,7 +28,15 @@ module Doorkeeper
26
28
  end
27
29
 
28
30
  def exists?(scope)
29
- @scopes.include? scope.to_s
31
+ scope = scope.to_s
32
+
33
+ @scopes.any? do |allowed_scope|
34
+ if dynamic_scopes_enabled? && dynamic_scopes_present?(allowed_scope, scope)
35
+ dynamic_scope_match?(allowed_scope, scope)
36
+ else
37
+ allowed_scope == scope
38
+ end
39
+ end
30
40
  end
31
41
 
32
42
  def add(*scopes)
@@ -66,6 +76,32 @@ module Doorkeeper
66
76
 
67
77
  private
68
78
 
79
+ def dynamic_scopes_enabled?
80
+ Doorkeeper.config.enable_dynamic_scopes?
81
+ end
82
+
83
+ def dynamic_scope_delimiter
84
+ return unless dynamic_scopes_enabled?
85
+
86
+ @dynamic_scope_delimiter ||= Doorkeeper.config.dynamic_scopes_delimiter
87
+ end
88
+
89
+ def dynamic_scopes_present?(allowed, requested)
90
+ allowed.include?(dynamic_scope_delimiter) && requested.include?(dynamic_scope_delimiter)
91
+ end
92
+
93
+ def dynamic_scope_match?(allowed, requested)
94
+ allowed_pattern = allowed.split(dynamic_scope_delimiter, 2)
95
+ request_pattern = requested.split(dynamic_scope_delimiter, 2)
96
+
97
+ return false if allowed_pattern[0] != request_pattern[0]
98
+ return false if allowed_pattern[1].blank?
99
+ return false if request_pattern[1].blank?
100
+ return true if allowed_pattern[1] == DYNAMIC_SCOPE_WILDCARD && allowed_pattern[1].present?
101
+
102
+ allowed_pattern[1] == request_pattern[1]
103
+ end
104
+
69
105
  def to_array(other)
70
106
  case other
71
107
  when Scopes
@@ -6,16 +6,15 @@ module Doorkeeper
6
6
  #
7
7
  # @see https://datatracker.ietf.org/doc/html/rfc7662
8
8
  class TokenIntrospection
9
- attr_reader :error
9
+ attr_reader :token, :error, :invalid_request_reason
10
10
 
11
11
  def initialize(server, token)
12
12
  @server = server
13
13
  @token = token
14
-
15
- authorize!
16
14
  end
17
15
 
18
16
  def authorized?
17
+ authorize!
19
18
  @error.blank?
20
19
  end
21
20
 
@@ -37,8 +36,7 @@ module Doorkeeper
37
36
 
38
37
  private
39
38
 
40
- attr_reader :server, :token
41
- attr_reader :invalid_request_reason
39
+ attr_reader :server
42
40
 
43
41
  # If the protected resource uses OAuth 2.0 client credentials to
44
42
  # authenticate to the introspection endpoint and its credentials are
@@ -60,24 +58,38 @@ module Doorkeeper
60
58
  def authorize!
61
59
  # Requested client authorization
62
60
  if server.credentials
63
- @error = Errors::InvalidClient unless authorized_client
61
+ authorize_using_basic_auth!
64
62
  elsif authorized_token
65
- # Requested bearer token authorization
66
- #
67
- # If the protected resource uses an OAuth 2.0 bearer token to authorize
68
- # its call to the introspection endpoint and the token used for
69
- # authorization does not contain sufficient privileges or is otherwise
70
- # invalid for this request, the authorization server responds with an
71
- # HTTP 401 code as described in Section 3 of OAuth 2.0 Bearer Token
72
- # Usage [RFC6750].
73
- #
74
- @error = Errors::InvalidToken unless valid_authorized_token?
63
+ authorize_using_bearer_token!
75
64
  else
76
65
  @error = Errors::InvalidRequest
77
66
  @invalid_request_reason = :request_not_authorized
78
67
  end
79
68
  end
80
69
 
70
+ def authorize_using_basic_auth!
71
+ # Note that a properly formed and authorized query for an inactive or
72
+ # otherwise invalid token (or a token the protected resource is not
73
+ # allowed to know about) is not considered an error response by this
74
+ # specification. In these cases, the authorization server MUST instead
75
+ # respond with an introspection response with the "active" field set to
76
+ # "false" as described in Section 2.2.
77
+ @error = Errors::InvalidClient unless authorized_client
78
+ end
79
+
80
+ def authorize_using_bearer_token!
81
+ # Requested bearer token authorization
82
+ #
83
+ # If the protected resource uses an OAuth 2.0 bearer token to authorize
84
+ # its call to the introspection endpoint and the token used for
85
+ # authorization does not contain sufficient privileges or is otherwise
86
+ # invalid for this request, the authorization server responds with an
87
+ # HTTP 401 code as described in Section 3 of OAuth 2.0 Bearer Token
88
+ # Usage [RFC6750].
89
+ #
90
+ @error = Errors::InvalidToken unless valid_authorized_token?
91
+ end
92
+
81
93
  # Client Authentication
82
94
  def authorized_client
83
95
  @authorized_client ||= server.credentials && server.client
@@ -30,6 +30,7 @@ module Doorkeeper
30
30
  {
31
31
  "Cache-Control" => "no-store, no-cache",
32
32
  "Content-Type" => "application/json; charset=utf-8",
33
+ "Pragma" => "no-cache",
33
34
  }
34
35
  end
35
36
  end
@@ -20,11 +20,13 @@ module Doorkeeper::Orm::ActiveRecord::Mixins
20
20
  dependent: :delete_all,
21
21
  class_name: Doorkeeper.config.access_token_class.to_s
22
22
 
23
- validates :name, :secret, :uid, presence: true
23
+ validates :name, :uid, presence: true
24
+ validates :secret, presence: true, if: -> { secret_required? }
24
25
  validates :uid, uniqueness: { case_sensitive: true }
25
- validates_with Doorkeeper::RedirectUriValidator, attributes: [:redirect_uri]
26
26
  validates :confidential, inclusion: { in: [true, false] }
27
27
 
28
+ validates_with Doorkeeper::RedirectUriValidator, attributes: [:redirect_uri]
29
+
28
30
  validate :scopes_match_configured, if: :enforce_scopes?
29
31
 
30
32
  before_validation :generate_uid, :generate_secret, on: :create
@@ -118,7 +120,7 @@ module Doorkeeper::Orm::ActiveRecord::Mixins
118
120
  end
119
121
 
120
122
  def generate_secret
121
- return if secret.present?
123
+ return if secret.present? || !secret_required?
122
124
 
123
125
  renew_secret
124
126
  end
@@ -136,6 +138,11 @@ module Doorkeeper::Orm::ActiveRecord::Mixins
136
138
  Doorkeeper.config.enforce_configured_scopes?
137
139
  end
138
140
 
141
+ def secret_required?
142
+ confidential? ||
143
+ !self.class.columns.detect { |column| column.name == "secret" }&.null
144
+ end
145
+
139
146
  # Helper method to extract collection of serializable attribute names
140
147
  # considering serialization options (like `only`, `except` and so on).
141
148
  #
@@ -4,8 +4,8 @@ module Doorkeeper
4
4
  module VERSION
5
5
  # Semantic versioning
6
6
  MAJOR = 5
7
- MINOR = 7
8
- TINY = 1
7
+ MINOR = 8
8
+ TINY = 0
9
9
  PRE = nil
10
10
 
11
11
  # Full version number
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Doorkeeper
7
+ # Generates migration with which drops NOT NULL constraint and allows not
8
+ # to bloat the database with redundant secret value.
9
+ #
10
+ class RemoveApplicationSecretNotNullConstraint < ::Rails::Generators::Base
11
+ include ::Rails::Generators::Migration
12
+ source_root File.expand_path("templates", __dir__)
13
+ desc "Removes NOT NULL constraint for OAuth2 applications."
14
+
15
+ def enable_polymorphic_resource_owner
16
+ migration_template(
17
+ "remove_applications_secret_not_null_constraint.rb.erb",
18
+ "db/migrate/remove_applications_secret_not_null_constraint.rb",
19
+ migration_version: migration_version,
20
+ )
21
+ end
22
+
23
+ def self.next_migration_number(dirname)
24
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
25
+ end
26
+
27
+ private
28
+
29
+ def migration_version
30
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
31
+ end
32
+ end
33
+ end
@@ -5,6 +5,7 @@ class CreateDoorkeeperTables < ActiveRecord::Migration<%= migration_version %>
5
5
  create_table :oauth_applications do |t|
6
6
  t.string :name, null: false
7
7
  t.string :uid, null: false
8
+ # Remove `null: false` or use conditional constraint if you are planning to use public clients.
8
9
  t.string :secret, null: false
9
10
 
10
11
  # Remove `null: false` if you are planning to use grant flows
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemoveApplicationsSecretNotNullConstraint < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ change_column_null :oauth_applications, :secret, true
6
+ end
7
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doorkeeper
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.7.1
4
+ version: 5.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Felipe Elias Philipp
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2024-06-25 00:00:00.000000000 Z
14
+ date: 2024-10-31 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: railties
@@ -305,6 +305,7 @@ files:
305
305
  - lib/generators/doorkeeper/migration_generator.rb
306
306
  - lib/generators/doorkeeper/pkce_generator.rb
307
307
  - lib/generators/doorkeeper/previous_refresh_token_generator.rb
308
+ - lib/generators/doorkeeper/remove_applications_secret_not_null_constraint_generator.rb
308
309
  - lib/generators/doorkeeper/templates/README
309
310
  - lib/generators/doorkeeper/templates/add_confidential_to_applications.rb.erb
310
311
  - lib/generators/doorkeeper/templates/add_owner_to_application_migration.rb.erb
@@ -313,6 +314,7 @@ files:
313
314
  - lib/generators/doorkeeper/templates/enable_polymorphic_resource_owner_migration.rb.erb
314
315
  - lib/generators/doorkeeper/templates/initializer.rb
315
316
  - lib/generators/doorkeeper/templates/migration.rb.erb
317
+ - lib/generators/doorkeeper/templates/remove_applications_secret_not_null_constraint.rb.erb
316
318
  - lib/generators/doorkeeper/views_generator.rb
317
319
  - vendor/assets/stylesheets/doorkeeper/bootstrap.min.css
318
320
  homepage: https://github.com/doorkeeper-gem/doorkeeper
@@ -346,7 +348,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
346
348
  - !ruby/object:Gem::Version
347
349
  version: '0'
348
350
  requirements: []
349
- rubygems_version: 3.2.3
351
+ rubygems_version: 3.5.15
350
352
  signing_key:
351
353
  specification_version: 4
352
354
  summary: OAuth 2 provider for Rails and Grape