doorkeeper 5.7.0 → 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: 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