doorkeeper 5.9.3 → 5.9.5

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: 71a32b8766b44f5eff16fbc128cfff6ab676b5af9104d16d58f060a0ed6d894c
4
- data.tar.gz: 864e07764f1ba86d6f52a10e5a081291c2748dcecbfb8bcc60b0f27baa4e616e
3
+ metadata.gz: d3e4ee342094b7729f29bb64d9045cfd9736d99388c5bb15d68568c6d59a8a54
4
+ data.tar.gz: e1549e74f00d26b85ed9afedbfaba17adce5a4e8a62c5ee01a8c4ff70c71bbcf
5
5
  SHA512:
6
- metadata.gz: 4116cfa7e4e06a1bff79489ddeced66af1803dedd6f2e1b1b5edd67db0d90e444922a87cfc2215a4e61357e54ddf988606c3ecce40e50afb24fdd5e06808a2bd
7
- data.tar.gz: 558ec2ac88215ba2cde034380a27d44d0c989841942c070e4ea02a746d154944b094ae2aa6a9284c7cca6d310705634fde4f6566fbf8a268a0c1b33e41937107
6
+ metadata.gz: d91e3f678a7a3d53e94c607b5b88d5a6e695e166a52fd9f5527c8988bd717c60debc292798c159da7356966baa4d26ad22d56a9b922582040e758c37ef26ea96
7
+ data.tar.gz: e335f698b2a3fe29b139e826fafada70b94fd239a0c2e0f2be762075c27bb73c8dab4c93a9581089214b07f9016af893cd0c70daf0fdc2db9c8b72f7ae848850
data/CHANGELOG.md CHANGED
@@ -9,6 +9,14 @@ User-visible changes worth mentioning.
9
9
 
10
10
  - Please add here
11
11
 
12
+ ## 5.9.5
13
+
14
+ - [#1901] Reject requests that authenticate the client with more than one method (RFC 6749 §2.3) with an `invalid_request` error, instead of silently authenticating with the first method that matched and discarding the other credentials.
15
+
16
+ ## 5.9.4
17
+
18
+ - [#1853] Fix `reuse_access_token` reusing a token that was created with `custom_access_token_attributes` values when the new request doesn't specify any custom attributes. Such requests now only match tokens without custom attributes.
19
+
12
20
  ## 5.9.3
13
21
 
14
22
  - [#1834] Fix default `allow_token_introspection` returning `false` when a custom `application_class` is configured. The default proc compared application objects with `==`, which fails when the authorized client and the introspected token's application are resolved as different classes (e.g. a base `Doorkeeper::Application` vs. a configured subclass) even though they reference the same record. It now compares application ids instead.
@@ -97,6 +97,7 @@ en:
97
97
  missing_param: 'Missing required parameter: %{value}.'
98
98
  request_not_authorized: 'Request needs to be authorized. Required parameter for authorizing the request is missing or invalid.'
99
99
  invalid_code_challenge: 'Code challenge is required.'
100
+ multiple_client_auth_methods: 'The request utilizes more than one mechanism for authenticating the client.'
100
101
  invalid_redirect_uri: "The requested redirect URI is malformed or doesn't match the client redirect URI."
101
102
  unauthorized_client: 'The client is not authorized to perform this request using this method.'
102
103
  access_denied: 'The resource owner or authorization server denied the request.'
@@ -24,6 +24,19 @@ module Doorkeeper
24
24
  end
25
25
  end
26
26
 
27
+ # Raised when a request uses more than one client authentication method,
28
+ # which RFC 6749 §2.3 explicitly forbids ("The client MUST NOT use more
29
+ # than one authentication method in each request").
30
+ class MultipleClientAuthMethods < DoorkeeperError
31
+ def type
32
+ :invalid_request
33
+ end
34
+
35
+ def reason
36
+ :multiple_client_auth_methods
37
+ end
38
+ end
39
+
27
40
  class MissingRequiredParameter < DoorkeeperError
28
41
  attr_reader :missing_param
29
42
 
@@ -56,7 +56,8 @@ module Doorkeeper
56
56
  OAuth::InvalidRequestResponse.new(
57
57
  name: exception.type,
58
58
  state: params[:state],
59
- missing_param: exception.missing_param,
59
+ missing_param: exception.try(:missing_param),
60
+ reason: exception.try(:reason),
60
61
  )
61
62
  else
62
63
  OAuth::ErrorResponse.new(name: exception.type, state: params[:state])
@@ -89,7 +89,8 @@ module Doorkeeper
89
89
  # @param custom_attributes [Nilable Hash]
90
90
  # A nil value, or hash with keys corresponding to the custom attributes
91
91
  # configured with the `custom_access_token_attributes` config option.
92
- # A nil value will ignore custom attributes.
92
+ # A nil value will ignore custom attributes, while an empty hash will
93
+ # only match tokens that have no custom attributes set.
93
94
  #
94
95
  # @return [Doorkeeper::AccessToken, nil] Access Token instance or
95
96
  # nil if matching record was not found
@@ -124,7 +125,8 @@ module Doorkeeper
124
125
  # @param custom_attributes [Nilable Hash]
125
126
  # A nil value, or hash with keys corresponding to the custom attributes
126
127
  # configured with the `custom_access_token_attributes` config option.
127
- # A nil value will ignore custom attributes.
128
+ # A nil value will ignore custom attributes, while an empty hash will
129
+ # only match tokens that have no custom attributes set.
128
130
  #
129
131
  # @return [Doorkeeper::AccessToken, nil] Access Token instance or
130
132
  # nil if matching record was not found
@@ -220,7 +222,10 @@ module Doorkeeper
220
222
  scopes = Doorkeeper::OAuth::Scopes.from_string(scopes) if scopes.is_a?(String)
221
223
 
222
224
  if Doorkeeper.config.reuse_access_token
223
- custom_attributes = extract_custom_attributes(token_attributes).presence
225
+ # An empty hash must stay distinct from nil here: nil ignores custom
226
+ # attributes when matching, while an empty hash only matches tokens
227
+ # that have no custom attributes set.
228
+ custom_attributes = extract_custom_attributes(token_attributes)
224
229
  access_token = matching_token_for(
225
230
  application, resource_owner, scopes, custom_attributes: custom_attributes, include_expired: false,
226
231
  )
@@ -5,12 +5,33 @@ module Doorkeeper
5
5
  class Client
6
6
  Credentials = Struct.new(:uid, :secret) do
7
7
  class << self
8
+ # Extracts the client credentials from the request using the
9
+ # configured extraction methods. The first method that yields
10
+ # credentials wins, as it always has.
11
+ #
12
+ # Raises Errors::MultipleClientAuthMethods when the request
13
+ # authenticates the client with more than one method, which RFC 6749
14
+ # §2.3 forbids ("The client MUST NOT use more than one authentication
15
+ # method in each request").
8
16
  def from_request(request, *credentials_methods)
9
- credentials_methods.inject(nil) do |_, method|
10
- method = self.method(method) if method.is_a?(Symbol)
11
- credentials = Credentials.new(*method.call(request))
12
- break credentials if credentials.present?
13
- end
17
+ # Callable extractors are opaque: they may legitimately overlap the
18
+ # built-in methods they are configured with, and evaluating all of
19
+ # them would also break the existing contract that the extractors
20
+ # after the matching one are not called. Such configurations keep
21
+ # the historical behaviour untouched.
22
+ return first_from_request(request, credentials_methods) unless credentials_methods.all?(Symbol)
23
+
24
+ credentials = credentials_methods.filter_map { |method| extract(request, method) }
25
+
26
+ # Only credentials carrying a secret authenticate the client. A bare
27
+ # uid is a public client identifying itself (RFC 6749 §2.3 "none"),
28
+ # not an authentication method of its own, so it doesn't count
29
+ # towards the single-method rule — RFC 7521 §4.2, for example,
30
+ # explicitly allows a client_id next to another authentication
31
+ # method.
32
+ raise Errors::MultipleClientAuthMethods if credentials.count { |c| c.secret.present? } > 1
33
+
34
+ credentials.first
14
35
  end
15
36
 
16
37
  def from_params(request)
@@ -23,6 +44,22 @@ module Doorkeeper
23
44
  Base64.decode64(Regexp.last_match(1)).split(/:/, 2)
24
45
  end
25
46
  end
47
+
48
+ private
49
+
50
+ def first_from_request(request, credentials_methods)
51
+ credentials_methods.inject(nil) do |_, method|
52
+ credentials = extract(request, method)
53
+ break credentials if credentials
54
+ end
55
+ end
56
+
57
+ # Returns the credentials the given method extracts from the request,
58
+ # or nil when it extracts none.
59
+ def extract(request, method)
60
+ method = self.method(method) if method.is_a?(Symbol)
61
+ Credentials.new(*method.call(request)).presence
62
+ end
26
63
  end
27
64
 
28
65
  # Public clients may have their secret blank, but "credentials" are
@@ -45,8 +45,11 @@ module Doorkeeper
45
45
  end
46
46
 
47
47
  def find_active_existing_token_for(client, scopes, attributes)
48
+ # An empty hash must stay distinct from nil here: nil ignores custom
49
+ # attributes when matching, while an empty hash only matches tokens
50
+ # that have no custom attributes set.
48
51
  custom_attributes = Doorkeeper.config.access_token_model
49
- .extract_custom_attributes(attributes).presence
52
+ .extract_custom_attributes(attributes)
50
53
  Doorkeeper.config.access_token_model.matching_token_for(
51
54
  client, nil, scopes, custom_attributes: custom_attributes, include_expired: false,
52
55
  )
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module OAuth
5
+ # In-memory representation of a verified JWT access token. Returned by
6
+ # OAuth::Token.authenticate when stateless_jwt_tokens is enabled, so that
7
+ # the existing doorkeeper_token / doorkeeper_authorize! helpers work
8
+ # without any database read.
9
+ #
10
+ # Stateless tokens cannot be revoked or refreshed server-side: #revoked?
11
+ # is always false and #revoke is a no-op. Rely on short token lifetimes
12
+ # (access_token_expires_in) for early invalidation.
13
+ class StatelessToken
14
+ attr_reader :claims, :application, :raw_token
15
+
16
+ def initialize(claims:, application: nil, raw_token: nil)
17
+ @claims = claims
18
+ @application = application
19
+ @raw_token = raw_token
20
+ end
21
+
22
+ def token_type
23
+ "Bearer"
24
+ end
25
+
26
+ # The raw token string. Used by introspection's
27
+ # authorized_token_matches_introspected? security check.
28
+ def token
29
+ @raw_token
30
+ end
31
+
32
+ # Issuance path: the freshly-signed JWT returned to the client.
33
+ def plaintext_token
34
+ @raw_token
35
+ end
36
+
37
+ def plaintext_refresh_token
38
+ nil
39
+ end
40
+
41
+ def resource_owner_id
42
+ claims["resource_owner_id"] || claims["sub"]
43
+ end
44
+
45
+ def application_id
46
+ application&.id
47
+ end
48
+
49
+ def scopes_string
50
+ claims["scope"].to_s
51
+ end
52
+
53
+ def scopes
54
+ OAuth::Scopes.from_string(scopes_string)
55
+ end
56
+
57
+ def includes_scope?(*required_scopes)
58
+ required_scopes.blank? || required_scopes.any? { |scope| scopes.exists?(scope.to_s) }
59
+ end
60
+
61
+ def custom_attributes
62
+ claims.slice(*Doorkeeper.config.custom_access_token_attributes.map(&:to_s))
63
+ end
64
+
65
+ # RFC 8707 resource indicator. Stored as a space-delimited string to
66
+ # match the AccessToken column shape consumed by token_introspection.
67
+ def resource
68
+ aud = claims["aud"]
69
+ return nil if aud.blank?
70
+
71
+ aud.is_a?(Array) ? aud.join(" ") : aud.to_s
72
+ end
73
+
74
+ def created_at
75
+ Time.at(claims["iat"].to_i).utc
76
+ end
77
+
78
+ def expires_in
79
+ claims["expires_in"]
80
+ end
81
+
82
+ def expires_at
83
+ return nil unless claims["exp"]
84
+
85
+ Time.at(claims["exp"].to_i).utc
86
+ end
87
+
88
+ def expires_in_seconds
89
+ return nil unless expires_at
90
+
91
+ expires = expires_at - Time.now.utc
92
+ sec = expires.seconds.round(0)
93
+ sec > 0 ? sec : 0
94
+ end
95
+
96
+ def expired?
97
+ return false unless expires_at
98
+
99
+ Time.now.utc > expires_at
100
+ end
101
+
102
+ def revoked?
103
+ false
104
+ end
105
+
106
+ def accessible?
107
+ !expired? && !revoked?
108
+ end
109
+
110
+ def acceptable?(required_scopes)
111
+ accessible? && includes_scope?(*required_scopes)
112
+ end
113
+
114
+ def revoke_previous_refresh_token!
115
+ # no-op: stateless tokens carry no refresh-token chain to revoke.
116
+ end
117
+
118
+ def revoke
119
+ # no-op: stateless tokens cannot be revoked server-side.
120
+ end
121
+
122
+ def revocable?
123
+ false
124
+ end
125
+
126
+ def as_json(_options = {})
127
+ {
128
+ resource_owner_id: resource_owner_id,
129
+ scope: scopes,
130
+ expires_in: expires_in_seconds,
131
+ application: { uid: application.try(:uid) },
132
+ created_at: created_at.to_i,
133
+ }.tap do |json|
134
+ json[:resource_owner_type] = claims["resource_owner_type"] if Doorkeeper.configuration.polymorphic_resource_owner?
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -5,7 +5,7 @@ module Doorkeeper
5
5
  # Semantic versioning
6
6
  MAJOR = 5
7
7
  MINOR = 9
8
- TINY = 3
8
+ TINY = 5
9
9
  PRE = nil
10
10
 
11
11
  # Full version number
metadata CHANGED
@@ -1,16 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doorkeeper
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.9.3
4
+ version: 5.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Felipe Elias Philipp
8
8
  - Tute Costa
9
9
  - Jon Moss
10
10
  - Nikita Bulai
11
+ autorequire:
11
12
  bindir: bin
12
13
  cert_chain: []
13
- date: 1980-01-02 00:00:00.000000000 Z
14
+ date: 2026-08-07 00:00:00.000000000 Z
14
15
  dependencies:
15
16
  - !ruby/object:Gem::Dependency
16
17
  name: railties
@@ -260,6 +261,7 @@ files:
260
261
  - lib/doorkeeper/oauth/pre_authorization.rb
261
262
  - lib/doorkeeper/oauth/refresh_token_request.rb
262
263
  - lib/doorkeeper/oauth/scopes.rb
264
+ - lib/doorkeeper/oauth/stateless_token.rb
263
265
  - lib/doorkeeper/oauth/token.rb
264
266
  - lib/doorkeeper/oauth/token_introspection.rb
265
267
  - lib/doorkeeper/oauth/token_request.rb
@@ -329,6 +331,7 @@ metadata:
329
331
  bug_tracker_uri: https://github.com/doorkeeper-gem/doorkeeper/issues
330
332
  documentation_uri: https://doorkeeper.gitbook.io/guides/
331
333
  funding_uri: https://opencollective.com/doorkeeper-gem
334
+ post_install_message:
332
335
  rdoc_options: []
333
336
  require_paths:
334
337
  - lib
@@ -343,7 +346,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
343
346
  - !ruby/object:Gem::Version
344
347
  version: '0'
345
348
  requirements: []
346
- rubygems_version: 4.0.11
349
+ rubygems_version: 3.5.16
350
+ signing_key:
347
351
  specification_version: 4
348
352
  summary: OAuth 2 provider for Rails and Grape
349
353
  test_files: []