doorkeeper 5.6.9 → 5.7.1

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: 6bf87231b7fa13bb47db61868954af3c01b07b0909ac95abb2de15d1064dd423
4
- data.tar.gz: f6dda19eae61f69331fc338fbb1803a7a94c563b4dc46ea694bee37c430db1b7
3
+ metadata.gz: df8ee24bf06e6b24c9ee822c24abf45ce0424b93ff05361dcdff76c930fa3c5a
4
+ data.tar.gz: 56e84b30480a60d02eea4b417f41c1cd6b322365bfce0e9fa31aad504def3807
5
5
  SHA512:
6
- metadata.gz: f0d54d75716f74f23f35ab9400683a805e3a4c7483c11ea5be10669abe38385c7625f9304379712f661bea026d49cb5fe8ddc1b6653cf66115279edc1f785096
7
- data.tar.gz: 68c841037b9544b1bdfdc36405169ad8cf942b6c32b6d1023261bb7c37116bab602a8c12b209d3b006219eaf0701d7a705ac6cdec49004c19ae918d044377d53
6
+ metadata.gz: d25945505890cb67e1e2db1e0a7eb8d49cd8bcccf05d2732883df6ace7269fe4bbe3de491a563ea3e254e1ac16e2daa407f665078cf243f7131a26db00b39842
7
+ data.tar.gz: 2269f220720be56f31928a1ab4180254058bc2b636994160db72030bc32f0a5db695e76b3186f6b0c24b8d77565bd4c49911d4f5a632b14c6bd57e213c278694
data/CHANGELOG.md CHANGED
@@ -7,7 +7,19 @@ User-visible changes worth mentioning.
7
7
 
8
8
  ## main
9
9
 
10
- - [#PR ID] Add your changelog here.
10
+ Add your entry here.
11
+
12
+ ## 5.7.1
13
+
14
+ - [#1705] Add `force_pkce` option that requires non-confidential clients to use PKCE when requesting an access_token using an authorization code
15
+
16
+ ## 5.7.0
17
+
18
+ - [#1696] Add missing `#issued_token` method to `OAuth::TokenResponse`
19
+ - [#1697] Allow a TokenResponse body to be customized (memoize response body).
20
+ - [#1702] Fix bugs for error response in the form_post and error view
21
+ - [#1660] Custom access token attributes are now considered when finding matching tokens (fixes #1665).
22
+ Introduce `revoke_previous_client_credentials_token` configuration option.
11
23
 
12
24
  ## 5.6.9
13
25
 
@@ -33,17 +45,17 @@ User-visible changes worth mentioning.
33
45
  - [#1648] Add custom token attributes to Refresh Token Request.
34
46
  - [#1649] Fixed custom_access_token_attributes related errors.
35
47
 
36
- # 5.6.5
48
+ ## 5.6.5
37
49
 
38
50
  - [#1602] Allow custom data to be stored inside access grants/tokens.
39
51
  - [#1634] Code refactoring for custom token attributes.
40
52
  - [#1639] Add grant type validation to avoid Internal Server Error for DELETE /oauth/authorize endpoint.
41
53
 
42
- # 5.6.4
54
+ ## 5.6.4
43
55
 
44
56
  - [#1633] Apply ORM configuration in #to_prepare block to avoid autoloading errors.
45
57
 
46
- # 5.6.3
58
+ ## 5.6.3
47
59
 
48
60
  - [#1622] Drop support for Rubies 2.5 and 2.6
49
61
  - [#1605] Fix URI validation for Ruby 3.2+.
@@ -31,7 +31,7 @@ module Doorkeeper
31
31
  private
32
32
 
33
33
  def render_success
34
- if skip_authorization? || (matching_token? && pre_auth.client.application.confidential?)
34
+ if skip_authorization? || can_authorize_response?
35
35
  redirect_or_render(authorize_response)
36
36
  elsif Doorkeeper.configuration.api_only
37
37
  render json: pre_auth
@@ -52,10 +52,16 @@ module Doorkeeper
52
52
  end
53
53
  end
54
54
 
55
+ def can_authorize_response?
56
+ Doorkeeper.config.custom_access_token_attributes.empty? && pre_auth.client.application.confidential? && matching_token?
57
+ end
58
+
55
59
  # Active access token issued for the same client and resource owner with
56
60
  # the same set of the scopes exists?
57
61
  def matching_token?
58
- Doorkeeper.config.access_token_model.matching_token_for(
62
+ # We don't match tokens on the custom attributes here - we're in the pre-auth here,
63
+ # so they haven't been supplied yet (there are no custom attributes to match on yet)
64
+ @matching_token ||= Doorkeeper.config.access_token_model.matching_token_for(
59
65
  pre_auth.client,
60
66
  current_resource_owner,
61
67
  pre_auth.scopes,
@@ -77,7 +83,7 @@ module Doorkeeper
77
83
  )
78
84
  end
79
85
  elsif pre_auth.form_post_response?
80
- render :form_post
86
+ render :form_post, locals: { auth: auth }
81
87
  else
82
88
  redirect_to auth.redirect_uri, allow_other_host: true
83
89
  end
@@ -4,6 +4,6 @@
4
4
 
5
5
  <main role="main">
6
6
  <pre>
7
- <%= (respond_to?(:error_response) ? error_response : @pre_auth.error_response).body[:error_description] %>
7
+ <%= (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description] %>
8
8
  </pre>
9
9
  </main>
@@ -3,7 +3,7 @@
3
3
  </header>
4
4
 
5
5
  <%= form_tag @pre_auth.redirect_uri, method: :post, name: :redirect_form, authenticity_token: false do %>
6
- <% @authorize_response.body.compact.each do |key, value| %>
6
+ <% auth.body.compact.each do |key, value| %>
7
7
  <%= hidden_field_tag key, value %>
8
8
  <% end %>
9
9
  <% end %>
@@ -106,6 +106,19 @@ module Doorkeeper
106
106
  @config.instance_variable_set(:@revoke_previous_client_credentials_token, true)
107
107
  end
108
108
 
109
+ # Only allow one valid access token obtained via authorization code
110
+ # per client. If a new access token is obtained before the old one
111
+ # expired, the old one gets revoked (disabled by default)
112
+ def revoke_previous_authorization_code_token
113
+ @config.instance_variable_set(:@revoke_previous_authorization_code_token, true)
114
+ end
115
+
116
+ # Require non-confidential apps to use PKCE (send a code_verifier) when requesting
117
+ # an access_token using an authorization code (disabled by default)
118
+ def force_pkce
119
+ @config.instance_variable_set(:@force_pkce, true)
120
+ end
121
+
109
122
  # Use an API mode for applications generated with --api argument
110
123
  # It will skip applications controller, disable forgery protection
111
124
  def api_only
@@ -481,6 +494,14 @@ module Doorkeeper
481
494
  option_set? :revoke_previous_client_credentials_token
482
495
  end
483
496
 
497
+ def revoke_previous_authorization_code_token?
498
+ option_set? :revoke_previous_authorization_code_token
499
+ end
500
+
501
+ def force_pkce?
502
+ option_set? :force_pkce
503
+ end
504
+
484
505
  def enforce_configured_scopes?
485
506
  option_set? :enforce_configured_scopes
486
507
  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
 
@@ -83,14 +83,18 @@ module Doorkeeper
83
83
  # Resource Owner model instance or it's ID
84
84
  # @param scopes [String, Doorkeeper::OAuth::Scopes]
85
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.
86
90
  #
87
91
  # @return [Doorkeeper::AccessToken, nil] Access Token instance or
88
92
  # nil if matching record was not found
89
93
  #
90
- def matching_token_for(application, resource_owner, scopes, include_expired: true)
94
+ def matching_token_for(application, resource_owner, scopes, custom_attributes: nil, include_expired: true)
91
95
  tokens = authorized_tokens_for(application&.id, resource_owner)
92
96
  tokens = tokens.not_expired unless include_expired
93
- find_matching_token(tokens, application, scopes)
97
+ find_matching_token(tokens, application, custom_attributes, scopes)
94
98
  end
95
99
 
96
100
  # Interface to enumerate access token records in batches in order not
@@ -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.
@@ -181,7 +215,9 @@ module Doorkeeper
181
215
  #
182
216
  def find_or_create_for(application:, resource_owner:, scopes:, **token_attributes)
183
217
  if Doorkeeper.config.reuse_access_token
184
- access_token = matching_token_for(application, resource_owner, scopes, include_expired: false)
218
+ custom_attributes = extract_custom_attributes(token_attributes).presence
219
+ access_token = matching_token_for(
220
+ application, resource_owner, scopes, custom_attributes: custom_attributes, include_expired: false)
185
221
 
186
222
  return access_token if access_token&.reusable?
187
223
  end
@@ -276,6 +312,18 @@ module Doorkeeper
276
312
  def fallback_secret_strategy
277
313
  ::Doorkeeper.config.token_secret_fallback_strategy
278
314
  end
315
+
316
+ # Extracts the token's custom attributes (defined by the
317
+ # custom_access_token_attributes config option) from the token's attributes.
318
+ #
319
+ # @param attributes [Hash]
320
+ # A hash of the access token's attributes.
321
+ # @return [Hash]
322
+ # A hash containing only the custom access token attributes.
323
+ def extract_custom_attributes(attributes)
324
+ attributes.with_indifferent_access.slice(
325
+ *Doorkeeper.configuration.custom_access_token_attributes)
326
+ end
279
327
  end
280
328
 
281
329
  # Access Token type: Bearer.
@@ -308,6 +356,14 @@ module Doorkeeper
308
356
  end
309
357
  end
310
358
 
359
+ # The token's custom attributes, as defined by
360
+ # the custom_access_token_attributes config option.
361
+ #
362
+ # @return [Hash] hash of custom access token attributes.
363
+ def custom_attributes
364
+ self.class.extract_custom_attributes(attributes)
365
+ end
366
+
311
367
  # Indicates whether the token instance have the same credential
312
368
  # as the other Access Token.
313
369
  #
@@ -29,6 +29,10 @@ module Doorkeeper
29
29
  grant.lock!
30
30
  raise Errors::InvalidGrantReuse if grant.revoked?
31
31
 
32
+ if Doorkeeper.config.revoke_previous_authorization_code_token?
33
+ revoke_previous_tokens(grant.application, resource_owner)
34
+ end
35
+
32
36
  grant.revoke
33
37
 
34
38
  find_or_create_access_token(
@@ -55,10 +59,16 @@ module Doorkeeper
55
59
  Doorkeeper.config.access_grant_model.pkce_supported?
56
60
  end
57
61
 
62
+ def confidential?
63
+ client&.confidential
64
+ end
65
+
58
66
  def validate_params
59
67
  @missing_param =
60
68
  if grant&.uses_pkce? && code_verifier.blank?
61
69
  :code_verifier
70
+ elsif !confidential? && Doorkeeper.config.force_pkce? && code_verifier.blank?
71
+ :code_verifier
62
72
  elsif redirect_uri.blank?
63
73
  :redirect_uri
64
74
  end
@@ -109,6 +119,10 @@ module Doorkeeper
109
119
  .slice(*Doorkeeper.config.custom_access_token_attributes)
110
120
  .symbolize_keys
111
121
  end
122
+
123
+ def revoke_previous_tokens(application, resource_owner)
124
+ Doorkeeper.config.access_token_model.revoke_all_for(application.id, resource_owner)
125
+ end
112
126
  end
113
127
  end
114
128
  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
@@ -8,7 +8,7 @@ module Doorkeeper
8
8
  existing_token = nil
9
9
 
10
10
  if lookup_existing_token?
11
- existing_token = find_active_existing_token_for(client, scopes)
11
+ existing_token = find_active_existing_token_for(client, scopes, attributes)
12
12
  return existing_token if Doorkeeper.config.reuse_access_token && existing_token&.reusable?
13
13
  end
14
14
 
@@ -44,8 +44,11 @@ module Doorkeeper
44
44
  Doorkeeper.config.revoke_previous_client_credentials_token?
45
45
  end
46
46
 
47
- def find_active_existing_token_for(client, scopes)
48
- Doorkeeper.config.access_token_model.matching_token_for(client, nil, scopes, include_expired: false)
47
+ def find_active_existing_token_for(client, scopes, attributes)
48
+ custom_attributes = Doorkeeper.config.access_token_model.
49
+ extract_custom_attributes(attributes).presence
50
+ Doorkeeper.config.access_token_model.matching_token_for(
51
+ client, nil, scopes, custom_attributes: custom_attributes, include_expired: false)
49
52
  end
50
53
  end
51
54
  end
@@ -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,
@@ -31,7 +32,7 @@ module Doorkeeper
31
32
  @code_challenge = parameters[:code_challenge]
32
33
  @code_challenge_method = parameters[:code_challenge_method]
33
34
  @resource_owner = resource_owner
34
- @custom_access_token_attributes = parameters.slice(*Doorkeeper.config.custom_access_token_attributes)
35
+ @custom_access_token_attributes = parameters.slice(*Doorkeeper.config.custom_access_token_attributes).to_h
35
36
  end
36
37
 
37
38
  def authorizable?
@@ -143,6 +144,12 @@ 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
 
@@ -5,12 +5,14 @@ module Doorkeeper
5
5
  class TokenResponse
6
6
  attr_reader :token
7
7
 
8
+ alias issued_token token
9
+
8
10
  def initialize(token)
9
11
  @token = token
10
12
  end
11
13
 
12
14
  def body
13
- {
15
+ @body ||= {
14
16
  "access_token" => token.plaintext_token,
15
17
  "token_type" => token.token_type,
16
18
  "expires_in" => token.expires_in_seconds,
@@ -4,8 +4,8 @@ module Doorkeeper
4
4
  module VERSION
5
5
  # Semantic versioning
6
6
  MAJOR = 5
7
- MINOR = 6
8
- TINY = 9
7
+ MINOR = 7
8
+ TINY = 1
9
9
  PRE = nil
10
10
 
11
11
  # Full version number
@@ -32,7 +32,7 @@ Doorkeeper.configure do
32
32
  # You can use your own model classes if you need to extend (or even override) default
33
33
  # Doorkeeper models such as `Application`, `AccessToken` and `AccessGrant.
34
34
  #
35
- # Be default Doorkeeper ActiveRecord ORM uses it's own classes:
35
+ # By default Doorkeeper ActiveRecord ORM uses its own classes:
36
36
  #
37
37
  # access_token_class "Doorkeeper::AccessToken"
38
38
  # access_grant_class "Doorkeeper::AccessGrant"
@@ -91,7 +91,10 @@ Doorkeeper.configure do
91
91
  # authorization_code_expires_in 10.minutes
92
92
 
93
93
  # Access token expiration time (default: 2 hours).
94
- # If you want to disable expiration, set this to `nil`.
94
+ # If you set this to `nil` Doorkeeper will not expire the token and omit expires_in in response.
95
+ # It is RECOMMENDED to set expiration time explicitly.
96
+ # Prefer access_token_expires_in 100.years or similar,
97
+ # which would be functionally equivalent and avoid the risk of unexpected behavior by callers.
95
98
  #
96
99
  # access_token_expires_in 2.hours
97
100
 
@@ -164,6 +167,17 @@ Doorkeeper.configure do
164
167
  #
165
168
  # revoke_previous_client_credentials_token
166
169
 
170
+ # Only allow one valid access token obtained via authorization code
171
+ # per client. If a new access token is obtained before the old one
172
+ # expired, the old one gets revoked (disabled by default)
173
+ #
174
+ # revoke_previous_authorization_code_token
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
+
167
181
  # Hash access and refresh tokens before persisting them.
168
182
  # This will disable the possibility to use +reuse_access_token+
169
183
  # since plain values can no longer be retrieved.
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.6.9
4
+ version: 5.7.1
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-02-14 00:00:00.000000000 Z
14
+ date: 2024-06-25 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: railties
@@ -346,7 +346,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
346
346
  - !ruby/object:Gem::Version
347
347
  version: '0'
348
348
  requirements: []
349
- rubygems_version: 3.1.6
349
+ rubygems_version: 3.2.3
350
350
  signing_key:
351
351
  specification_version: 4
352
352
  summary: OAuth 2 provider for Rails and Grape