doorkeeper 6.0.0.beta1 → 6.0.0.beta2

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -1
  3. data/README.md +203 -0
  4. data/app/controllers/doorkeeper/applications_controller.rb +38 -9
  5. data/app/controllers/doorkeeper/authorizations_controller.rb +17 -3
  6. data/app/controllers/doorkeeper/tokens_controller.rb +5 -1
  7. data/app/views/doorkeeper/authorizations/new.html.erb +18 -0
  8. data/config/locales/en.yml +2 -0
  9. data/lib/doorkeeper/client_authentication/legacy_callable.rb +6 -0
  10. data/lib/doorkeeper/client_authentication/method.rb +17 -0
  11. data/lib/doorkeeper/client_authentication/verified_credentials.rb +19 -0
  12. data/lib/doorkeeper/client_authentication.rb +9 -0
  13. data/lib/doorkeeper/config.rb +47 -0
  14. data/lib/doorkeeper/document_cache.rb +81 -0
  15. data/lib/doorkeeper/errors.rb +48 -0
  16. data/lib/doorkeeper/http_fetcher.rb +232 -0
  17. data/lib/doorkeeper/models/access_grant_mixin.rb +16 -0
  18. data/lib/doorkeeper/models/access_token_mixin.rb +42 -1
  19. data/lib/doorkeeper/oauth/authorization/code.rb +10 -0
  20. data/lib/doorkeeper/oauth/authorization/token.rb +13 -2
  21. data/lib/doorkeeper/oauth/authorization_code_request.rb +123 -2
  22. data/lib/doorkeeper/oauth/base_request.rb +1 -2
  23. data/lib/doorkeeper/oauth/client.rb +6 -0
  24. data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +4 -0
  25. data/lib/doorkeeper/oauth/client_authentication/client_secret_post.rb +4 -0
  26. data/lib/doorkeeper/oauth/client_authentication/none.rb +47 -8
  27. data/lib/doorkeeper/oauth/client_authentication/private_key_jwt/key_resolver.rb +104 -0
  28. data/lib/doorkeeper/oauth/client_authentication/private_key_jwt/replay_guard.rb +78 -0
  29. data/lib/doorkeeper/oauth/client_authentication/private_key_jwt.rb +247 -0
  30. data/lib/doorkeeper/oauth/client_credentials/creator.rb +25 -9
  31. data/lib/doorkeeper/oauth/client_credentials_request.rb +36 -5
  32. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +35 -23
  33. data/lib/doorkeeper/oauth/metadata_response.rb +8 -0
  34. data/lib/doorkeeper/oauth/password_access_token_request.rb +27 -1
  35. data/lib/doorkeeper/oauth/pre_authorization.rb +51 -3
  36. data/lib/doorkeeper/oauth/refresh_token_request.rb +51 -1
  37. data/lib/doorkeeper/oauth/resource_indicator_validator.rb +70 -0
  38. data/lib/doorkeeper/oauth/scopes.rb +25 -0
  39. data/lib/doorkeeper/oauth/token_introspection.rb +6 -0
  40. data/lib/doorkeeper/version.rb +1 -1
  41. data/lib/doorkeeper.rb +4 -0
  42. data/lib/generators/doorkeeper/grant_reuse_revocation_generator.rb +43 -0
  43. data/lib/generators/doorkeeper/resource_indicators_generator.rb +33 -0
  44. data/lib/generators/doorkeeper/templates/add_access_token_to_access_grants.rb.erb +10 -0
  45. data/lib/generators/doorkeeper/templates/enable_resource_indicators_migration.rb.erb +8 -0
  46. data/lib/generators/doorkeeper/templates/initializer.rb +65 -0
  47. data/lib/generators/doorkeeper/templates/migration.rb.erb +13 -0
  48. metadata +45 -3
@@ -15,27 +15,42 @@ module Doorkeeper
15
15
  false
16
16
  end
17
17
 
18
+ # RFC6749, Section 3.1.2.3 requires the requested redirect URI to be
19
+ # compared to the registered redirect URIs using the simple string
20
+ # comparison defined in RFC3986, Section 6.2.1.
21
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2.3
18
22
  def self.matches?(url, client_url)
23
+ return true if url == client_url
24
+
25
+ # RFC8252, Paragraph 7.3 allows the port of loopback interface
26
+ # redirect URIs to vary at runtime, so it is ignored when both
27
+ # URIs point to the loopback interface.
28
+ # @see https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
19
29
  url = as_uri(url)
20
30
  client_url = as_uri(client_url)
21
31
 
22
- unless client_url.query.nil?
23
- return false unless query_matches?(url.query, client_url.query)
24
-
25
- # Clear out queries so rest of URI can be tested. This allows query
26
- # params to be in the request but order not mattering.
27
- client_url.query = nil
28
- end
32
+ return false unless loopback_uri?(url) && loopback_uri?(client_url)
29
33
 
30
- # RFC8252, Paragraph 7.3
31
- # @see https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
32
- if loopback_uri?(url) && loopback_uri?(client_url)
33
- url.port = nil
34
- client_url.port = nil
35
- end
34
+ urls_match?(url, client_url)
35
+ rescue URI::InvalidURIError
36
+ false
37
+ end
36
38
 
37
- url.query = nil
38
- url == client_url
39
+ # RFC 8252 §7.3 lets only the PORT of a loopback redirect URI vary at
40
+ # runtime. Compare every other component explicitly instead of blanking
41
+ # the port and comparing the reassembled strings: `URI#port=` also
42
+ # clears the userinfo on Ruby >= 4.0, which would let
43
+ # `http://attacker@127.0.0.1/cb` match a registered
44
+ # `http://127.0.0.1/cb`. Comparing components (not `URI#==`) also keeps
45
+ # the exception from widening past the port — e.g. an empty path is not
46
+ # treated as equal to "/".
47
+ def self.urls_match?(url, client_url)
48
+ url.scheme == client_url.scheme &&
49
+ url.userinfo == client_url.userinfo &&
50
+ url.host == client_url.host &&
51
+ url.path == client_url.path &&
52
+ url.query == client_url.query &&
53
+ url.fragment == client_url.fragment
39
54
  end
40
55
 
41
56
  def self.loopback_uri?(uri)
@@ -45,6 +60,11 @@ module Doorkeeper
45
60
  end
46
61
 
47
62
  def self.valid_for_authorization?(url, client_url)
63
+ # client_url is blank when the application was registered without a
64
+ # redirect URI (allow_blank_redirect_uri) — no possible match for
65
+ # redirection based OAuth flows, not an error.
66
+ return false if client_url.blank?
67
+
48
68
  valid?(url) && client_url.split.any? { |other_url| matches?(url, other_url) }
49
69
  end
50
70
 
@@ -52,14 +72,6 @@ module Doorkeeper
52
72
  URI.parse(url)
53
73
  end
54
74
 
55
- def self.query_matches?(query, client_query)
56
- return true if client_query.blank? && query.blank?
57
- return false if client_query.nil? || query.nil?
58
-
59
- # Will return true independent of query order
60
- client_query.split("&").sort == query.split("&").sort
61
- end
62
-
63
75
  def self.valid_scheme?(uri)
64
76
  return false if uri.scheme.blank?
65
77
 
@@ -35,6 +35,8 @@ module Doorkeeper
35
35
  # condition under which the iss parameter is emitted. false survives
36
36
  # the compaction below, so it is advertised explicitly.
37
37
  authorization_response_iss_parameter_supported: config.issuer.present?,
38
+ # RFC 8707: advertise resource indicator support when configured.
39
+ resource_indicators_supported: resource_indicators_supported?,
38
40
  }
39
41
  data.compact!
40
42
 
@@ -152,6 +154,12 @@ module Doorkeeper
152
154
  def code_challenge_methods_supported
153
155
  config.pkce_code_challenge_methods_supported
154
156
  end
157
+
158
+ def resource_indicators_supported?
159
+ config.resource_indicator_validator.present? &&
160
+ Doorkeeper.config.access_grant_model.resource_indicators_supported? &&
161
+ Doorkeeper.config.access_token_model.resource_indicators_supported?
162
+ end
155
163
  end
156
164
  end
157
165
  end
@@ -9,10 +9,12 @@ module Doorkeeper
9
9
  validate :client_supports_grant_flow, error: Errors::UnauthorizedClient
10
10
  validate :resource_owner, error: Errors::InvalidGrant
11
11
  validate :scopes, error: Errors::InvalidScope
12
+ validate :resource_indicators, error: Errors::InvalidTarget
12
13
 
13
14
  attr_reader :client, :credentials, :resource_owner, :parameters, :access_token
14
15
 
15
16
  def initialize(server, client, credentials, resource_owner, parameters = {})
17
+ super()
16
18
  @server = server
17
19
  @resource_owner = resource_owner
18
20
  @client = client
@@ -20,12 +22,21 @@ module Doorkeeper
20
22
  @parameters = parameters
21
23
  @original_scopes = parameters[:scope]
22
24
  @grant_type = Doorkeeper::OAuth::PASSWORD
25
+ @raw_resource_indicators = parameters[:resource]
23
26
  end
24
27
 
25
28
  private
26
29
 
27
30
  def before_successful_response
28
- find_or_create_access_token(client, resource_owner, scopes, {}, server)
31
+ token_attributes = {}
32
+ if @resolved_resource_indicators.present?
33
+ unless Doorkeeper.config.access_token_model.resource_indicators_supported?
34
+ raise Errors::MissingResourceColumn, "oauth_access_tokens"
35
+ end
36
+
37
+ token_attributes[:resource] = @resolved_resource_indicators.join(" ")
38
+ end
39
+ find_or_create_access_token(client, resource_owner, scopes, token_attributes, server)
29
40
  super
30
41
  end
31
42
 
@@ -70,6 +81,21 @@ module Doorkeeper
70
81
  def validate_client_supports_grant_flow
71
82
  Doorkeeper.config.allow_grant_flow_for_client?(grant_type, client&.application)
72
83
  end
84
+
85
+ # RFC 8707: validate resource indicators against server policy.
86
+ def validate_resource_indicators
87
+ validator = Doorkeeper.config.resource_indicator_validator
88
+ return true unless validator
89
+
90
+ @resolved_resource_indicators = ResourceIndicatorValidator.validate!(
91
+ @raw_resource_indicators,
92
+ config_validator: validator,
93
+ client: client,
94
+ )
95
+ true
96
+ rescue Errors::InvalidTarget
97
+ false
98
+ end
73
99
  end
74
100
  end
75
101
  end
@@ -16,11 +16,12 @@ module Doorkeeper
16
16
  validate :scopes, error: Errors::InvalidScope
17
17
  validate :code_challenge, error: Errors::InvalidRequest
18
18
  validate :code_challenge_method, error: Errors::InvalidCodeChallengeMethod
19
+ validate :resource_indicators, error: Errors::InvalidTarget
19
20
 
20
21
  attr_reader :client, :code_challenge, :code_challenge_method, :missing_param,
21
22
  :redirect_uri, :resource_owner, :response_type, :state,
22
23
  :authorization_response_flow, :response_mode, :custom_access_token_attributes,
23
- :invalid_request_reason
24
+ :invalid_request_reason, :resource_indicators
24
25
 
25
26
  def initialize(server, parameters = {}, resource_owner = nil)
26
27
  @server = server
@@ -34,6 +35,7 @@ module Doorkeeper
34
35
  @code_challenge_method = parameters[:code_challenge_method]
35
36
  @resource_owner = resource_owner
36
37
  @custom_access_token_attributes = parameters.slice(*Doorkeeper.config.custom_access_token_attributes).to_h
38
+ @raw_resource_indicators = parameters[:resource]
37
39
  end
38
40
 
39
41
  def authorizable?
@@ -84,7 +86,7 @@ module Doorkeeper
84
86
  if client_scopes.blank?
85
87
  server.default_scopes.to_s
86
88
  else
87
- server.default_scopes.allowed(client_scopes).to_s
89
+ server.default_scopes.common(client_scopes).to_s
88
90
  end
89
91
  end
90
92
 
@@ -123,7 +125,21 @@ module Doorkeeper
123
125
  :scope
124
126
  end
125
127
 
126
- @missing_param.nil?
128
+ return false unless @missing_param.nil?
129
+
130
+ # A structured `scope` (e.g. `scope[a]=b`, parsed by Rack into a Hash)
131
+ # is malformed (RFC 6749 §3.3). Reject it here — before validate_scopes
132
+ # reaches Scopes.from_string — so the authorization endpoint answers
133
+ # invalid_request instead of letting the raised error surface as a 500.
134
+ # Set a reason so the error_description is not translated from nil.
135
+ return true if scope_param_well_formed?
136
+
137
+ @invalid_request_reason = :unknown
138
+ false
139
+ end
140
+
141
+ def scope_param_well_formed?
142
+ @scope.nil? || @scope.is_a?(String)
127
143
  end
128
144
 
129
145
  def validate_response_type
@@ -155,6 +171,14 @@ module Doorkeeper
155
171
 
156
172
  def validate_code_challenge
157
173
  return true unless Doorkeeper.config.force_pkce?
174
+ # PKCE (RFC 7636) protects the exchange of an authorization code, so
175
+ # a code_challenge is only required from response types that issue one
176
+ # ("code" and code-carrying hybrid types like "code id_token"). For
177
+ # response types that never issue a code (e.g. "token" or an OIDC
178
+ # extension's "id_token"), there is no token-endpoint exchange where a
179
+ # verifier could ever be checked, so requiring a challenge would
180
+ # reject those requests over a parameter that cannot be validated.
181
+ return true unless code_issuing_response_type?
158
182
  return true if code_challenge.present?
159
183
 
160
184
  @invalid_request_reason = :invalid_code_challenge
@@ -168,6 +192,21 @@ module Doorkeeper
168
192
  (code_challenge_method.present? && Doorkeeper.config.pkce_code_challenge_methods_supported.include?(code_challenge_method))
169
193
  end
170
194
 
195
+ def validate_resource_indicators
196
+ validator = Doorkeeper.config.resource_indicator_validator
197
+ # When no validator is configured, resource indicators are ignored (feature disabled)
198
+ return true unless validator
199
+
200
+ @resource_indicators = ResourceIndicatorValidator.validate!(
201
+ @raw_resource_indicators,
202
+ config_validator: validator,
203
+ client: client,
204
+ )
205
+ true
206
+ rescue Errors::InvalidTarget
207
+ false
208
+ end
209
+
171
210
  def response_on_fragment?
172
211
  return response_type == "token" if response_mode.nil?
173
212
 
@@ -178,6 +217,15 @@ module Doorkeeper
178
217
  response_type == "code" ? AUTHORIZATION_CODE : IMPLICIT
179
218
  end
180
219
 
220
+ # Whether the requested response type issues an authorization code.
221
+ # Multi-valued response types (OIDC hybrid flows registered by
222
+ # extensions, e.g. "code id_token") are space-delimited per OAuth 2.0
223
+ # Multiple Response Type Encoding Practices, so a token-wise check also
224
+ # covers response types Doorkeeper itself does not ship.
225
+ def code_issuing_response_type?
226
+ response_type.to_s.split.include?("code")
227
+ end
228
+
181
229
  def pre_auth_hash
182
230
  {
183
231
  client_id: client.uid,
@@ -10,6 +10,7 @@ module Doorkeeper
10
10
  validate :client, error: Errors::InvalidClient
11
11
  validate :client_match, error: Errors::InvalidGrant
12
12
  validate :scope, error: Errors::InvalidScope
13
+ validate :resource_indicators, error: Errors::InvalidTarget
13
14
 
14
15
  attr_reader :access_token, :client, :credentials, :refresh_token
15
16
  attr_reader :missing_param
@@ -21,13 +22,21 @@ module Doorkeeper
21
22
  @grant_type = Doorkeeper::OAuth::REFRESH_TOKEN
22
23
  @original_scopes = parameters[:scope] || parameters[:scopes]
23
24
  @refresh_token_parameter = parameters[:refresh_token]
25
+ @raw_resource_indicators = parameters[:resource]
24
26
  @client = load_client(credentials) if credentials
25
27
  end
26
28
 
27
29
  private
28
30
 
31
+ # Resolved through +OAuth::Client+ rather than by looking the row up
32
+ # directly, so this grant applies the same rules the other token
33
+ # endpoint grants do: credentials that carry no secret because their
34
+ # authentication method already proved the client's identity
35
+ # (private_key_jwt) resolve by uid alone. The application record is
36
+ # what is returned, since that is what +#client+ has always exposed
37
+ # here.
29
38
  def load_client(credentials)
30
- Doorkeeper.config.application_model.by_uid_and_secret(credentials.uid, credentials.secret)
39
+ Doorkeeper::OAuth::Client.authenticate(credentials)&.application
31
40
  end
32
41
 
33
42
  def before_successful_response
@@ -74,6 +83,19 @@ module Doorkeeper
74
83
  attributes[:previous_refresh_token] = refresh_token.refresh_token
75
84
  end
76
85
 
86
+ # RFC 8707: carry resource indicators to the new access token.
87
+ # If the refresh request specified a (subset of) resource(s), use those;
88
+ # otherwise inherit from the refresh token itself.
89
+ if @resolved_resource_indicators.present?
90
+ unless Doorkeeper.config.access_token_model.resource_indicators_supported?
91
+ raise Errors::MissingResourceColumn, "oauth_access_tokens"
92
+ end
93
+
94
+ attributes[:resource] = @resolved_resource_indicators.join(" ")
95
+ elsif refresh_token.try(:resource).present?
96
+ attributes[:resource] = refresh_token.resource
97
+ end
98
+
77
99
  # RFC6749
78
100
  # 1.5. Refresh Token
79
101
  #
@@ -132,6 +154,34 @@ module Doorkeeper
132
154
  end
133
155
  end
134
156
 
157
+ # RFC 8707: resource indicators on refresh must be a subset of those
158
+ # bound to the original refresh token (which inherited from the grant).
159
+ #
160
+ # Subset and syntax enforcement run even when no validator is configured
161
+ # as long as the original token is already audience-restricted: a refresh
162
+ # must never widen the audience beyond what the original token carried.
163
+ # Only when the feature is disabled AND the original token has no stored
164
+ # resources is the `resource` parameter ignored entirely.
165
+ def validate_resource_indicators
166
+ original_resources = refresh_token.try(:resource)&.split
167
+
168
+ validator = Doorkeeper.config.resource_indicator_validator
169
+
170
+ # Feature effectively off: no validator and nothing already bound to
171
+ # enforce against. Ignore the `resource` parameter.
172
+ return true if validator.nil? && original_resources.blank?
173
+
174
+ @resolved_resource_indicators = ResourceIndicatorValidator.validate!(
175
+ @raw_resource_indicators,
176
+ config_validator: validator,
177
+ client: client,
178
+ grant_resource_indicators: original_resources,
179
+ )
180
+ true
181
+ rescue Errors::InvalidTarget
182
+ false
183
+ end
184
+
135
185
  def custom_token_attributes_with_data
136
186
  refresh_token
137
187
  .attributes
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Doorkeeper
6
+ module OAuth
7
+ # Validates the `resource` parameter per RFC 8707.
8
+ #
9
+ # The `resource` value MUST be an absolute URI without a fragment component.
10
+ # Multiple `resource` parameters MAY be present.
11
+ #
12
+ # @see https://datatracker.ietf.org/doc/html/rfc8707#section-2
13
+ module ResourceIndicatorValidator
14
+ module_function
15
+
16
+ # Validates and normalizes an array of resource indicator values.
17
+ #
18
+ # @param resource_indicators [Array<String>, String, nil] One or more resource URIs
19
+ # @param config_validator [Proc, nil] Custom validator from configuration
20
+ # @param client [Doorkeeper::OAuth::Client, nil] The requesting client
21
+ # @param grant_resource_indicators [Array<String>, nil] Resource indicators from the
22
+ # original authorization grant (for subset enforcement on token requests)
23
+ #
24
+ # @return [Array<String>] Validated resource indicator URIs
25
+ # @raise [Doorkeeper::Errors::InvalidTarget] When validation fails
26
+ def validate!(resource_indicators, config_validator: nil, client: nil, grant_resource_indicators: nil)
27
+ return [] if resource_indicators.blank?
28
+
29
+ indicators = Array.wrap(resource_indicators).reject(&:blank?).uniq
30
+ return [] if indicators.empty?
31
+
32
+ indicators.each { |uri| validate_uri!(uri) }
33
+
34
+ # If the token request specifies resources, they must be a subset of those
35
+ # originally granted (RFC 8707 §2.2).
36
+ raise Errors::InvalidTarget if grant_resource_indicators.present? && (indicators - grant_resource_indicators).any?
37
+
38
+ # Custom server policy validation
39
+ raise Errors::InvalidTarget if config_validator && !config_validator.call(indicators, client)
40
+
41
+ indicators
42
+ end
43
+
44
+ # @return [Boolean] true when the values are syntactically valid
45
+ def valid?(resource_indicators)
46
+ return true if resource_indicators.blank?
47
+
48
+ Array(resource_indicators).reject(&:blank?).all? { |uri| valid_uri?(uri) }
49
+ end
50
+
51
+ def valid_uri?(uri)
52
+ return false unless uri.is_a?(String)
53
+
54
+ parsed = URI.parse(uri)
55
+ # MUST be absolute URI
56
+ return false unless parsed.absolute?
57
+ # MUST NOT include a fragment
58
+ return false if parsed.fragment
59
+
60
+ true
61
+ rescue URI::InvalidURIError, TypeError
62
+ false
63
+ end
64
+
65
+ def validate_uri!(uri)
66
+ raise Errors::InvalidTarget unless valid_uri?(uri)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -10,6 +10,14 @@ module Doorkeeper
10
10
 
11
11
  def self.from_string(string)
12
12
  string ||= ""
13
+
14
+ # A non-string scope (e.g. `scope[a]=b`, parsed by Rack into a Hash)
15
+ # has no octets to split into tokens. Reject it as a malformed request
16
+ # (RFC 6749 §3.3); at the token endpoint TokensController's rescue_from
17
+ # turns this into invalid_request. Callers that always pass a string
18
+ # (the model layer) never hit this.
19
+ raise Errors::InvalidScopeParameter, "scope must be a String" unless string.is_a?(String)
20
+
13
21
  new.tap do |scope|
14
22
  scope.add(*string.split)
15
23
  end
@@ -92,6 +100,23 @@ module Doorkeeper
92
100
  self.class.from_array(filtered_scopes)
93
101
  end
94
102
 
103
+ # Returns the scopes present in both sets, taking dynamic scopes
104
+ # into account: a dynamic scope pattern (e.g. `user:*`) on either
105
+ # side matches the concrete scope (e.g. `user:1`) on the other,
106
+ # and the concrete scope is what ends up in the result. Unlike
107
+ # #allowed, the same scopes are returned regardless of which set is
108
+ # the receiver, though the order may differ: scopes matched from the
109
+ # argument come first, followed by any scope only the receiver
110
+ # contributes (a concrete scope matched by a pattern in the argument).
111
+ #
112
+ # @param other The set of scopes to intersect with
113
+ def common(other)
114
+ other = self.class.from_array(to_array(other))
115
+ matched = other.select { |scope| exists?(scope) } +
116
+ select { |scope| other.exists?(scope) }
117
+ self.class.from_array(matched)
118
+ end
119
+
95
120
  private
96
121
 
97
122
  def dynamic_scopes_enabled?
@@ -113,6 +113,12 @@ module Doorkeeper
113
113
  iat: @token.created_at.to_i,
114
114
  }
115
115
 
116
+ # RFC 8707: include audience restriction when resource indicators are present
117
+ if @token.try(:resource).present?
118
+ aud = @token.resource.split
119
+ response[:aud] = aud.length == 1 ? aud.first : aud
120
+ end
121
+
116
122
  # `token_type` (RFC 6749 §7.1) and `exp` describe the access token:
117
123
  # a refresh token is not a Bearer credential and has no expiry of its
118
124
  # own, so both are omitted (they are OPTIONAL per RFC 7662 §2.2) when
@@ -6,7 +6,7 @@ module Doorkeeper
6
6
  MAJOR = 6
7
7
  MINOR = 0
8
8
  TINY = 0
9
- PRE = "beta1"
9
+ PRE = "beta2"
10
10
 
11
11
  # Full version number
12
12
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
data/lib/doorkeeper.rb CHANGED
@@ -8,7 +8,9 @@ require "doorkeeper/engine"
8
8
  module Doorkeeper
9
9
  autoload :Errors, "doorkeeper/errors"
10
10
  autoload :ClientAuthentication, "doorkeeper/client_authentication"
11
+ autoload :DocumentCache, "doorkeeper/document_cache"
11
12
  autoload :GrantFlow, "doorkeeper/grant_flow"
13
+ autoload :HttpFetcher, "doorkeeper/http_fetcher"
12
14
  autoload :OAuth, "doorkeeper/oauth"
13
15
  autoload :Rake, "doorkeeper/rake"
14
16
  autoload :Request, "doorkeeper/request"
@@ -58,6 +60,7 @@ module Doorkeeper
58
60
  autoload :PasswordAccessTokenRequest, "doorkeeper/oauth/password_access_token_request"
59
61
  autoload :PreAuthorization, "doorkeeper/oauth/pre_authorization"
60
62
  autoload :RefreshTokenRequest, "doorkeeper/oauth/refresh_token_request"
63
+ autoload :ResourceIndicatorValidator, "doorkeeper/oauth/resource_indicator_validator"
61
64
  autoload :Scopes, "doorkeeper/oauth/scopes"
62
65
  autoload :Token, "doorkeeper/oauth/token"
63
66
  autoload :TokenIntrospection, "doorkeeper/oauth/token_introspection"
@@ -68,6 +71,7 @@ module Doorkeeper
68
71
  autoload :None, "doorkeeper/oauth/client_authentication/none"
69
72
  autoload :ClientSecretBasic, "doorkeeper/oauth/client_authentication/client_secret_basic"
70
73
  autoload :ClientSecretPost, "doorkeeper/oauth/client_authentication/client_secret_post"
74
+ autoload :PrivateKeyJwt, "doorkeeper/oauth/client_authentication/private_key_jwt"
71
75
  end
72
76
 
73
77
  module Authorization
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Doorkeeper
7
+ # Generates migration to add the access token reference column to the
8
+ # access grants table, enabling revocation of previously issued tokens
9
+ # when an authorization code is reused (RFC 6749 §4.1.2, §10.5).
10
+ #
11
+ class GrantReuseRevocationGenerator < ::Rails::Generators::Base
12
+ include ::Rails::Generators::Migration
13
+ source_root File.expand_path("templates", __dir__)
14
+ desc "Support revoking issued tokens on authorization code reuse"
15
+
16
+ def self.next_migration_number(path)
17
+ ActiveRecord::Generators::Base.next_migration_number(path)
18
+ end
19
+
20
+ def grant_reuse_revocation
21
+ return unless no_access_token_id_column?
22
+
23
+ migration_template(
24
+ "add_access_token_to_access_grants.rb.erb",
25
+ "db/migrate/add_access_token_to_access_grants.rb",
26
+ migration_version: migration_version,
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ def migration_version
33
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
34
+ end
35
+
36
+ def no_access_token_id_column?
37
+ !ActiveRecord::Base.connection.column_exists?(
38
+ :oauth_access_grants,
39
+ :access_token_id,
40
+ )
41
+ end
42
+ end
43
+ end
@@ -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 the `resource` column for access grants and
8
+ # access tokens, required for RFC 8707 Resource Indicators support.
9
+ #
10
+ class ResourceIndicatorsGenerator < ::Rails::Generators::Base
11
+ include ::Rails::Generators::Migration
12
+ source_root File.expand_path("templates", __dir__)
13
+ desc "Add resource indicators support (RFC 8707) to Doorkeeper tables."
14
+
15
+ def resource_indicators
16
+ migration_template(
17
+ "enable_resource_indicators_migration.rb.erb",
18
+ "db/migrate/enable_resource_indicators.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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddAccessTokenToAccessGrants < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ # Indexed: when a code is replayed, the revocation checks whether the
6
+ # issued token is still referenced by another grant, which filters
7
+ # oauth_access_grants by access_token_id.
8
+ add_reference :oauth_access_grants, :access_token, index: true
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EnableResourceIndicators < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ add_column :oauth_access_grants, :resource, :text, null: true
6
+ add_column :oauth_access_tokens, :resource, :text, null: true
7
+ end
8
+ end