doorkeeper 5.7.0 → 5.8.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aeb5db3b840c0c214129e69fb029c1daf95974b033856ab550e9abd3b529b0c1
4
- data.tar.gz: 6f948a7fa2b8a2796c56e94a762b1e256228c13f972bbd056d1b4e246bea5001
3
+ metadata.gz: f4907be020648eb5c1dbc6e596c31c72c4133740e849ee1345ff337a4ce01c56
4
+ data.tar.gz: 831e491b48efe921e43065d393c6a785a9988e6057f1ed3e1e4f2e8626555e65
5
5
  SHA512:
6
- metadata.gz: 2da3aca020ed25ba334d5dfbcbffd1c46818eaa79bbc67e38bcf01dada8105c6683bfb5552dae1cdce34a68e5baafa0c9a9b49b5c980f78464925f2f7576b873
7
- data.tar.gz: 865a9c03a28b309624e8058a251c92d002199f9072015efafafb622fad5878274042bee32aba169a0395fc1ba0558ca86c1bd7b32b9dc288c5136b69c6a68e69
6
+ metadata.gz: 2f97a8c5d6749cea33b31737988b26271d1ad03f4bd8f003b3674e8e81159a44e6a54f5611b868a06a9a074995bcc47165f222aadcd961746cc43776ee993c57
7
+ data.tar.gz: 3d2e13efa8bba0cedc4a63055d6ecb70f0af8d1dc0644047b4b1ecb3bc3bf5affd2a0a20d46989a4999b4361573c39ae72f516398c67ffe708512f0fd6e23011
data/CHANGELOG.md CHANGED
@@ -9,6 +9,20 @@ 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
+
22
+ ## 5.7.1
23
+
24
+ - [#1705] Add `force_pkce` option that requires non-confidential clients to use PKCE when requesting an access_token using an authorization code
25
+
12
26
  ## 5.7.0
13
27
 
14
28
  - [#1696] Add missing `#issued_token` method to `OAuth::TokenResponse`
@@ -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)
@@ -113,6 +123,12 @@ module Doorkeeper
113
123
  @config.instance_variable_set(:@revoke_previous_authorization_code_token, true)
114
124
  end
115
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
+
116
132
  # Use an API mode for applications generated with --api argument
117
133
  # It will skip applications controller, disable forgery protection
118
134
  def api_only
@@ -237,6 +253,7 @@ module Doorkeeper
237
253
  option :orm, default: :active_record
238
254
  option :native_redirect_uri, default: "urn:ietf:wg:oauth:2.0:oob", deprecated: true
239
255
  option :grant_flows, default: %w[authorization_code client_credentials]
256
+ option :pkce_code_challenge_methods, default: %w[plain S256]
240
257
  option :handle_auth_errors, default: :render
241
258
  option :token_lookup_batch_size, default: 10_000
242
259
  # Sets the token_reuse_limit
@@ -412,7 +429,7 @@ module Doorkeeper
412
429
  default: (lambda do |token, authorized_client, authorized_token|
413
430
  if authorized_token
414
431
  authorized_token.application == token&.application
415
- elsif token.application
432
+ elsif token&.application
416
433
  authorized_client == token.application
417
434
  else
418
435
  true
@@ -492,6 +509,10 @@ module Doorkeeper
492
509
  option_set? :revoke_previous_authorization_code_token
493
510
  end
494
511
 
512
+ def force_pkce?
513
+ option_set? :force_pkce
514
+ end
515
+
495
516
  def enforce_configured_scopes?
496
517
  option_set? :enforce_configured_scopes
497
518
  end
@@ -500,6 +521,14 @@ module Doorkeeper
500
521
  option_set? :enable_application_owner
501
522
  end
502
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
+
503
532
  def polymorphic_resource_owner?
504
533
  option_set? :polymorphic_resource_owner
505
534
  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
@@ -54,6 +54,7 @@ module Doorkeeper
54
54
  InvalidClient = Class.new(BaseResponseError)
55
55
  InvalidScope = Class.new(BaseResponseError)
56
56
  InvalidRedirectUri = Class.new(BaseResponseError)
57
+ InvalidCodeChallenge = Class.new(BaseResponseError)
57
58
  InvalidCodeChallengeMethod = Class.new(BaseResponseError)
58
59
  InvalidGrant = Class.new(BaseResponseError)
59
60
 
@@ -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(
@@ -59,10 +59,16 @@ module Doorkeeper
59
59
  Doorkeeper.config.access_grant_model.pkce_supported?
60
60
  end
61
61
 
62
+ def confidential?
63
+ client&.confidential
64
+ end
65
+
62
66
  def validate_params
63
67
  @missing_param =
64
68
  if grant&.uses_pkce? && code_verifier.blank?
65
69
  :code_verifier
70
+ elsif !confidential? && Doorkeeper.config.force_pkce? && code_verifier.blank?
71
+ :code_verifier
66
72
  elsif redirect_uri.blank?
67
73
  :redirect_uri
68
74
  end
@@ -5,7 +5,7 @@ module Doorkeeper
5
5
  class Client
6
6
  attr_reader :application
7
7
 
8
- delegate :id, :name, :uid, :redirect_uri, :scopes, to: :@application
8
+ delegate :id, :name, :uid, :redirect_uri, :scopes, :confidential, to: :@application
9
9
 
10
10
  def initialize(application)
11
11
  @application = application
@@ -14,6 +14,7 @@ module Doorkeeper
14
14
  validate :response_type, error: Errors::UnsupportedResponseType
15
15
  validate :response_mode, error: Errors::UnsupportedResponseMode
16
16
  validate :scopes, error: Errors::InvalidScope
17
+ validate :code_challenge, error: Errors::InvalidCodeChallenge
17
18
  validate :code_challenge_method, error: Errors::InvalidCodeChallengeMethod
18
19
 
19
20
  attr_reader :client, :code_challenge, :code_challenge_method, :missing_param,
@@ -143,11 +144,17 @@ module Doorkeeper
143
144
  )
144
145
  end
145
146
 
147
+ def validate_code_challenge
148
+ return true unless Doorkeeper.config.force_pkce?
149
+ return true if client.confidential
150
+ code_challenge.present?
151
+ end
152
+
146
153
  def validate_code_challenge_method
147
154
  return true unless Doorkeeper.config.access_grant_model.pkce_supported?
148
155
 
149
156
  code_challenge.blank? ||
150
- (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))
151
158
  end
152
159
 
153
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,7 +4,7 @@ module Doorkeeper
4
4
  module VERSION
5
5
  # Semantic versioning
6
6
  MAJOR = 5
7
- MINOR = 7
7
+ MINOR = 8
8
8
  TINY = 0
9
9
  PRE = nil
10
10
 
@@ -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
@@ -173,6 +173,11 @@ Doorkeeper.configure do
173
173
  #
174
174
  # revoke_previous_authorization_code_token
175
175
 
176
+ # Require non-confidential clients to use PKCE when using an authorization code
177
+ # to obtain an access_token (disabled by default)
178
+ #
179
+ # force_pkce
180
+
176
181
  # Hash access and refresh tokens before persisting them.
177
182
  # This will disable the possibility to use +reuse_access_token+
178
183
  # since plain values can no longer be retrieved.
@@ -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.0
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-04-24 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