doorkeeper 5.9.9 → 6.0.0.beta1

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -22
  3. data/README.md +1 -0
  4. data/app/controllers/doorkeeper/authorizations_controller.rb +3 -33
  5. data/app/controllers/doorkeeper/authorized_applications_controller.rb +0 -23
  6. data/app/controllers/doorkeeper/metadata_controller.rb +20 -0
  7. data/app/controllers/doorkeeper/tokens_controller.rb +10 -1
  8. data/app/views/doorkeeper/authorizations/new.html.erb +6 -0
  9. data/lib/doorkeeper/client_authentication/credentials.rb +11 -0
  10. data/lib/doorkeeper/client_authentication/fallback_method.rb +19 -0
  11. data/lib/doorkeeper/client_authentication/legacy_callable.rb +46 -0
  12. data/lib/doorkeeper/client_authentication/method.rb +23 -0
  13. data/lib/doorkeeper/client_authentication/registry.rb +47 -0
  14. data/lib/doorkeeper/client_authentication.rb +93 -0
  15. data/lib/doorkeeper/config/option.rb +1 -1
  16. data/lib/doorkeeper/config/validations.rb +162 -1
  17. data/lib/doorkeeper/config.rb +114 -15
  18. data/lib/doorkeeper/errors.rb +1 -0
  19. data/lib/doorkeeper/models/access_token_mixin.rb +40 -5
  20. data/lib/doorkeeper/models/application_mixin.rb +14 -6
  21. data/lib/doorkeeper/models/concerns/secret_storable.rb +10 -1
  22. data/lib/doorkeeper/oauth/authorization/uri_builder.rb +11 -0
  23. data/lib/doorkeeper/oauth/authorization_code_request.rb +4 -1
  24. data/lib/doorkeeper/oauth/client.rb +18 -0
  25. data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +53 -0
  26. data/lib/doorkeeper/oauth/client_authentication/client_secret_post.rb +29 -0
  27. data/lib/doorkeeper/oauth/client_authentication/none.rb +32 -0
  28. data/lib/doorkeeper/oauth/code_response.rb +16 -2
  29. data/lib/doorkeeper/oauth/error_response.rb +11 -2
  30. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +0 -14
  31. data/lib/doorkeeper/oauth/metadata_response.rb +157 -0
  32. data/lib/doorkeeper/oauth/pre_authorization.rb +12 -32
  33. data/lib/doorkeeper/oauth/token.rb +2 -95
  34. data/lib/doorkeeper/oauth/token_introspection.rb +27 -7
  35. data/lib/doorkeeper/orm/active_record/mixins/application.rb +2 -2
  36. data/lib/doorkeeper/orm/active_record/redirect_uri_validator.rb +1 -5
  37. data/lib/doorkeeper/rails/routes/mapping.rb +1 -0
  38. data/lib/doorkeeper/rails/routes.rb +6 -0
  39. data/lib/doorkeeper/request.rb +65 -18
  40. data/lib/doorkeeper/server.rb +5 -2
  41. data/lib/doorkeeper/version.rb +4 -4
  42. data/lib/doorkeeper.rb +8 -4
  43. data/lib/generators/doorkeeper/templates/initializer.rb +67 -18
  44. metadata +14 -7
  45. data/lib/doorkeeper/oauth/client/credentials.rb +0 -86
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module OAuth
5
+ module ClientAuthentication
6
+ # RFC 6749 §2.3.1 "client_secret_post": client credentials are sent in
7
+ # the request body. The query string is intentionally ignored so that
8
+ # credentials must be supplied in the body as the spec requires.
9
+ class ClientSecretPost
10
+ def self.matches_request?(request)
11
+ params = request.request_parameters.with_indifferent_access
12
+
13
+ request.post? &&
14
+ params[:client_id].present? &&
15
+ params[:client_secret].present?
16
+ end
17
+
18
+ def self.authenticate(request)
19
+ params = request.request_parameters.with_indifferent_access
20
+
21
+ Doorkeeper::ClientAuthentication::Credentials.new(
22
+ params[:client_id],
23
+ params[:client_secret],
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module OAuth
5
+ module ClientAuthentication
6
+ # RFC 6749 §2.3 "none": a public client that authenticates with only a
7
+ # client_id and no secret (in the request body, not the query string).
8
+ class None
9
+ # Requires the Authorization header to be absent or blank: a request
10
+ # that carries a non-blank one (Basic, Bearer, or anything else) is
11
+ # attempting header-based authentication and must not be silently
12
+ # treated as an unauthenticated public client. This is narrower than
13
+ # the legacy +from_params+ extractor, which read the body +client_id+
14
+ # regardless of the Authorization header.
15
+ def self.matches_request?(request)
16
+ params = request.request_parameters.with_indifferent_access
17
+
18
+ request.post? &&
19
+ request.authorization.blank? &&
20
+ params[:client_id].present? &&
21
+ params[:client_secret].blank?
22
+ end
23
+
24
+ def self.authenticate(request)
25
+ params = request.request_parameters.with_indifferent_access
26
+
27
+ Doorkeeper::ClientAuthentication::Credentials.new(params[:client_id], nil)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -28,17 +28,22 @@ module Doorkeeper
28
28
  token_type: auth.token.token_type,
29
29
  expires_in: auth.token.expires_in_seconds,
30
30
  state: pre_auth.state,
31
- }
31
+ }.merge(iss_parameter)
32
32
  elsif auth.try(:access_grant?)
33
33
  {
34
34
  code: auth.token.plaintext_token,
35
35
  state: pre_auth.state,
36
- }
36
+ }.merge(iss_parameter)
37
37
  end
38
38
  end
39
39
 
40
40
  def redirect_uri
41
41
  if URIChecker.oob_uri?(pre_auth.redirect_uri)
42
+ # Out-of-band "redirects" render the code/token on an authorization
43
+ # server page for the user to copy manually; there is no redirect
44
+ # back to the client. RFC 9207 scopes the iss parameter to the
45
+ # authorization response sent to the client, so - like state, which
46
+ # oob_redirect also omits - iss is intentionally not carried here.
42
47
  auth.oob_redirect
43
48
  elsif response_on_fragment
44
49
  Authorization::URIBuilder.uri_with_fragment(pre_auth.redirect_uri, body)
@@ -46,6 +51,15 @@ module Doorkeeper
46
51
  Authorization::URIBuilder.uri_with_query(pre_auth.redirect_uri, body)
47
52
  end
48
53
  end
54
+
55
+ private
56
+
57
+ # RFC 9207 Authorization Server Issuer Identification: advertise the
58
+ # issuer in the authorization response only when an issuer is configured.
59
+ def iss_parameter
60
+ issuer = Doorkeeper.config.issuer
61
+ issuer.present? ? { iss: issuer } : {}
62
+ end
49
63
  end
50
64
  end
51
65
  end
@@ -5,7 +5,7 @@ module Doorkeeper
5
5
  class ErrorResponse < BaseResponse
6
6
  include OAuth::Helpers
7
7
 
8
- NON_REDIRECTABLE_STATES = %i[invalid_redirect_uri invalid_client unauthorized_client].freeze
8
+ NON_REDIRECTABLE_STATES = %i[invalid_redirect_uri invalid_client].freeze
9
9
 
10
10
  def self.from_request(request, attributes = {})
11
11
  new(
@@ -38,6 +38,7 @@ module Doorkeeper
38
38
  @exception_class = attributes[:exception_class]
39
39
  @redirect_uri = attributes[:redirect_uri]
40
40
  @response_on_fragment = attributes[:response_on_fragment]
41
+ @issuer = attributes[:issuer]
41
42
  end
42
43
 
43
44
  def body
@@ -45,11 +46,19 @@ module Doorkeeper
45
46
  error: name,
46
47
  error_description: description,
47
48
  state: state,
49
+ # RFC 9207 scopes the issuer to authorization error responses sent to
50
+ # the client, i.e. the ones redirected back to its redirect_uri. It is
51
+ # supplied only by the authorization endpoint (never token,
52
+ # introspection or protected resource), and gated on redirectable? so
53
+ # non-redirectable errors (invalid_client, invalid_redirect_uri) and
54
+ # out-of-band flows - which render to the user instead of redirecting
55
+ # to the client - do not carry it. Blank values are dropped below.
56
+ iss: (@issuer if redirectable?),
48
57
  }.reject { |_, v| v.blank? }
49
58
  end
50
59
 
51
60
  def status
52
- if name == :invalid_client || name == :unauthorized_client
61
+ if name == :invalid_client
53
62
  :unauthorized
54
63
  else
55
64
  :bad_request
@@ -6,15 +6,6 @@ module Doorkeeper
6
6
  module OAuth
7
7
  module Helpers
8
8
  module URIChecker
9
- # Schemes a user agent evaluates in the document that navigates to
10
- # them instead of fetching a resource from somewhere else. With
11
- # `response_mode=form_post` that document is the authorization
12
- # server's own page, so a redirect URI with one of these schemes would
13
- # run the client's script on the server's origin. None of them can be
14
- # a legitimate redirection endpoint (RFC 6749 Section 3.1.2), so they
15
- # are refused regardless of the `forbid_redirect_uri` configuration.
16
- SCRIPT_SCHEMES = %w[javascript vbscript data].freeze
17
-
18
9
  def self.valid?(url)
19
10
  return true if oob_uri?(url)
20
11
 
@@ -71,15 +62,10 @@ module Doorkeeper
71
62
 
72
63
  def self.valid_scheme?(uri)
73
64
  return false if uri.scheme.blank?
74
- return false if script_scheme?(uri)
75
65
 
76
66
  %w[localhost].exclude?(uri.scheme)
77
67
  end
78
68
 
79
- def self.script_scheme?(uri)
80
- SCRIPT_SCHEMES.include?(uri.scheme.to_s.downcase)
81
- end
82
-
83
69
  def self.hypertext_scheme?(uri)
84
70
  %w[http https].include?(uri.scheme)
85
71
  end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module OAuth
5
+ # OAuth 2.0 Authorization Server Metadata response as described in
6
+ # RFC 8414 (https://www.rfc-editor.org/rfc/rfc8414).
7
+ class MetadataResponse < BaseResponse
8
+ def initialize(base_url, url_builder)
9
+ super()
10
+ @base_url = base_url
11
+ @url_builder = url_builder
12
+ end
13
+
14
+ def body
15
+ @body ||= begin
16
+ data = {
17
+ # presence: a blank issuer (e.g. an unset env var) must fall back
18
+ # to base_url, matching the issuer.present? condition below so the
19
+ # two fields cannot disagree.
20
+ issuer: issuer.presence || @base_url,
21
+ # Only advertise endpoints whose controllers are installed. A
22
+ # controller disabled through skip_controllers has no routes mapping,
23
+ # so endpoint_for resolves it to nil and it is dropped below.
24
+ authorization_endpoint: authorization_endpoint,
25
+ token_endpoint: token_endpoint,
26
+ revocation_endpoint: revocation_endpoint,
27
+ introspection_endpoint: (introspection_endpoint if introspection_enabled?),
28
+ scopes_supported: scopes_supported,
29
+ response_types_supported: response_types_supported,
30
+ response_modes_supported: response_modes_supported,
31
+ grant_types_supported: grant_types_supported,
32
+ token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported,
33
+ code_challenge_methods_supported: code_challenge_methods_supported,
34
+ # RFC 9207: true only when an issuer is configured, matching the
35
+ # condition under which the iss parameter is emitted. false survives
36
+ # the compaction below, so it is advertised explicitly.
37
+ authorization_response_iss_parameter_supported: config.issuer.present?,
38
+ }
39
+ data.compact!
40
+
41
+ # userinfo_endpoint is intentionally advertised as null for backwards
42
+ # compatibility; it is meant to be populated through custom_metadata
43
+ # (e.g. by an OIDC extension), so it must survive the compaction above.
44
+ data[:userinfo_endpoint] = userinfo_endpoint
45
+
46
+ data.merge(custom_metadata)
47
+ end
48
+ end
49
+
50
+ def status
51
+ :ok
52
+ end
53
+
54
+ def headers
55
+ {
56
+ "Cache-Control" => "public",
57
+ "Content-Type" => "application/json; charset=utf-8",
58
+ }
59
+ end
60
+
61
+ private
62
+
63
+ def config
64
+ @config ||= Doorkeeper.configuration
65
+ end
66
+
67
+ def url_for(**args)
68
+ @url_builder.call(**args)
69
+ end
70
+
71
+ # Resolve the URL for the given route group, or nil when the group has no
72
+ # routes mapping (i.e. the controller was disabled via skip_controllers).
73
+ def endpoint_for(group, action:)
74
+ mapping = Doorkeeper::Rails::Routes.mapping[group]
75
+ return unless mapping
76
+
77
+ # The leading slash anchors the controller name: without it, Rails
78
+ # resolves the name relative to the current (doorkeeper/metadata)
79
+ # namespace whenever their segment depths differ, e.g. a configured
80
+ # `controllers tokens: "custom_tokens"` would be looked up as
81
+ # "doorkeeper/custom_tokens" and raise ActionController::UrlGenerationError.
82
+ url_for(controller: "/#{mapping[:controllers]}", action: action)
83
+ end
84
+
85
+ def custom_metadata
86
+ config.custom_metadata.symbolize_keys
87
+ end
88
+
89
+ def issuer
90
+ config.issuer
91
+ end
92
+
93
+ def authorization_endpoint
94
+ endpoint_for(:authorizations, action: "new")
95
+ end
96
+
97
+ def token_endpoint
98
+ endpoint_for(:tokens, action: "create")
99
+ end
100
+
101
+ def userinfo_endpoint
102
+ nil
103
+ end
104
+
105
+ def revocation_endpoint
106
+ endpoint_for(:tokens, action: "revoke")
107
+ end
108
+
109
+ def introspection_endpoint
110
+ endpoint_for(:tokens, action: "introspect")
111
+ end
112
+
113
+ def introspection_enabled?
114
+ Doorkeeper.configured? &&
115
+ !config.allow_token_introspection.is_a?(FalseClass)
116
+ end
117
+
118
+ def scopes_supported
119
+ config.scopes.to_a
120
+ end
121
+
122
+ def response_types_supported
123
+ config.authorization_response_types
124
+ end
125
+
126
+ def response_modes_supported
127
+ config.authorization_response_flows.flat_map(&:response_mode_matches).uniq
128
+ end
129
+
130
+ def grant_types_supported
131
+ # RFC 8414 lists token-endpoint grant type values (e.g.
132
+ # "authorization_code", "refresh_token"), so derive them from the
133
+ # flows that actually handle a grant_type and use their grant_type
134
+ # value rather than the configured flow name. This drops
135
+ # response-type-only flows such as :implicit and reports the real
136
+ # grant type for flows whose name differs from it (e.g. a URN).
137
+ config.token_grant_types
138
+ end
139
+
140
+ # The resolved methods (rather than the raw client_authentication names)
141
+ # reflect what the server actually accepts: unregistered names are
142
+ # dropped by the resolver, and a deprecated client_credentials-only
143
+ # configuration is honored as the source of truth. Legacy callable
144
+ # extractors have no registered method name a client could use, so they
145
+ # are not advertised.
146
+ def token_endpoint_auth_methods_supported
147
+ config.client_authentication_methods.filter_map do |method|
148
+ method.name.to_s if Doorkeeper::ClientAuthentication.get(method.name)
149
+ end
150
+ end
151
+
152
+ def code_challenge_methods_supported
153
+ config.pkce_code_challenge_methods_supported
154
+ end
155
+ end
156
+ end
157
+ end
@@ -5,19 +5,11 @@ module Doorkeeper
5
5
  class PreAuthorization
6
6
  include Validations
7
7
 
8
- # The validations that identify the client and its redirect URI. None of
9
- # them depend on the resource owner, and none of their failures leaves a
10
- # redirect target worth trusting: RFC 6749 Section 4.1.2.1 (Section
11
- # 4.2.2.1 for the implicit flow) asks for the resource owner to be
12
- # informed when the client_id is missing or invalid, and Section 3.1.2.4
13
- # asks the same for the redirect URI.
14
- CLIENT_VALIDATIONS = %i[client_id client redirect_uri].freeze
15
-
16
8
  validate :client_id, error: Errors::InvalidRequest
17
9
  validate :client, error: Errors::InvalidClient
18
- validate :client_supports_grant_flow, error: Errors::UnauthorizedClient
19
- validate :resource_owner_authorize_for_client, error: Errors::InvalidClient
20
10
  validate :redirect_uri, error: Errors::InvalidRedirectUri
11
+ validate :client_supports_grant_flow, error: Errors::UnauthorizedClient
12
+ validate :resource_owner_authorize_for_client, error: Errors::AccessDenied
21
13
  validate :params, error: Errors::InvalidRequest
22
14
  validate :response_type, error: Errors::UnsupportedResponseType
23
15
  validate :response_mode, error: Errors::UnsupportedResponseMode
@@ -48,25 +40,6 @@ module Doorkeeper
48
40
  valid?
49
41
  end
50
42
 
51
- # Runs only CLIENT_VALIDATIONS, in declared order, so a request naming an
52
- # unknown client or an invalid redirect URI can be refused on its own -
53
- # the validations in between (client_supports_grant_flow,
54
- # resource_owner_authorize_for_client) answer a different question and
55
- # are left to a full #validate run.
56
- def client_valid?
57
- @error = nil
58
- @missing_param = nil
59
-
60
- self.class.validations.each do |validation|
61
- next unless CLIENT_VALIDATIONS.include?(validation[:attribute])
62
-
63
- @error = validation[:options][:error] unless send("validate_#{validation[:attribute]}")
64
- break if @error
65
- end
66
-
67
- @error.nil?
68
- end
69
-
70
43
  def scopes
71
44
  Scopes.from_string(scope)
72
45
  end
@@ -76,13 +49,21 @@ module Doorkeeper
76
49
  end
77
50
 
78
51
  def error_response
52
+ # RFC 9207: authorization error responses advertise the issuer. Passing
53
+ # the configured issuer here (nil when unset) scopes the iss parameter
54
+ # to the authorization endpoint only.
79
55
  if error == Errors::InvalidRequest
80
56
  OAuth::InvalidRequestResponse.from_request(
81
57
  self,
82
58
  response_on_fragment: response_on_fragment?,
59
+ issuer: Doorkeeper.config.issuer,
83
60
  )
84
61
  else
85
- OAuth::ErrorResponse.from_request(self, response_on_fragment: response_on_fragment?)
62
+ OAuth::ErrorResponse.from_request(
63
+ self,
64
+ response_on_fragment: response_on_fragment?,
65
+ issuer: Doorkeeper.config.issuer,
66
+ )
86
67
  end
87
68
  end
88
69
 
@@ -174,7 +155,6 @@ module Doorkeeper
174
155
 
175
156
  def validate_code_challenge
176
157
  return true unless Doorkeeper.config.force_pkce?
177
- return true if client.confidential
178
158
  return true if code_challenge.present?
179
159
 
180
160
  @invalid_request_reason = :invalid_code_challenge
@@ -207,7 +187,7 @@ module Doorkeeper
207
187
  scope: scope,
208
188
  client_name: client.name,
209
189
  status: I18n.t("doorkeeper.pre_authorization.status"),
210
- }
190
+ }.reverse_merge(custom_access_token_attributes.symbolize_keys)
211
191
  end
212
192
  end
213
193
  end
@@ -3,54 +3,8 @@
3
3
  module Doorkeeper
4
4
  module OAuth
5
5
  class Token
6
- # Built-in extractors that read a request parameter, and the parameter
7
- # each of them reads. RFC 6750 treats the form-encoded body (§2.2) and
8
- # the URI query string (§2.3) as two distinct transmission methods, but
9
- # Rack and ActionDispatch both collapse them into a single parameter
10
- # hash — ActionDispatch lets the query win, Rack lets the body win — so
11
- # a request carrying the parameter in both would present a single value
12
- # to the extractor and never be refused. The multi-method check reads
13
- # the two sources separately for these extractors; selection keeps
14
- # using the extractor, so which one wins is unchanged.
15
- PARAMETER_EXTRACTORS = {
16
- from_access_token_param: "access_token",
17
- from_bearer_param: "bearer_token",
18
- }.freeze
19
-
20
6
  class << self
21
- # RFC 6750 §2: "Clients MUST NOT use more than one method to transmit
22
- # the token in each request", and §3.1 lists using more than one method
23
- # among the conditions an invalid_request answers. Returning the first
24
- # method that yields a value would discard every other token presented
25
- # in the same request with no error, warning or log entry, leaving
26
- # which token authorizes the request to be decided by the configured
27
- # order of +access_token_methods+ rather than by what the caller sent —
28
- # so a layer in front of Doorkeeper that reads a different one of them
29
- # can reach a different verdict about the very same request.
30
- #
31
- # What §2 forbids is using more than one method, so the check counts
32
- # transmission methods rather than comparing the tokens they carry:
33
- # the same value presented twice is still two methods. Refusal answers
34
- # nil: the caller is told the request carries no usable token and
35
- # fails closed with the invalid_token (401) response, keeping every
36
- # calling contract on this stable branch intact — the invalid_request
37
- # (400) that §3.1 strictly prescribes needs a new error and render
38
- # path, so it ships with 6.0 only.
39
- #
40
- # Only the built-in extractors — symbols naming methods on this class,
41
- # all of them side-effect-free reads of the request — take part in
42
- # that check. A custom callable extractor is a configuration adapter
43
- # rather than a transmission method: it keeps the historical
44
- # first-wins selection and is never invoked more than once, the same
45
- # exemption client authentication gives its legacy callable
46
- # extractors (Request#validate_client_authentication!).
47
7
  def from_request(request, *methods)
48
- used = methods.sum do |method|
49
- method.is_a?(Symbol) ? transmission_methods_used(request, method) : 0
50
- end
51
-
52
- return if used > 1
53
-
54
8
  methods.inject(nil) do |_, method|
55
9
  method = self.method(method) if method.is_a?(Symbol)
56
10
  credentials = method.call(request)
@@ -69,11 +23,11 @@ module Doorkeeper
69
23
  end
70
24
 
71
25
  def from_access_token_param(request)
72
- parameters(request)[:access_token]
26
+ request.parameters[:access_token]
73
27
  end
74
28
 
75
29
  def from_bearer_param(request)
76
- parameters(request)[:bearer_token]
30
+ request.parameters[:bearer_token]
77
31
  end
78
32
 
79
33
  def from_bearer_authorization(request)
@@ -90,53 +44,6 @@ module Doorkeeper
90
44
 
91
45
  private
92
46
 
93
- # How many transmission methods the given built-in extractor finds a
94
- # token in. Usually one, or none — but for the parameter extractors
95
- # above it is the body and the query counted separately, since the
96
- # parameter hash collapsed them into the single value the extractor
97
- # reads. That value is counted on its own only when neither raw source
98
- # explains it, so a token reaching the extractor by some other route —
99
- # an :access_token path segment, or a host that overrode the extractor
100
- # — is still counted exactly once rather than twice.
101
- def transmission_methods_used(request, method)
102
- value = self.method(method).call(request).presence
103
-
104
- parameter = PARAMETER_EXTRACTORS[method]
105
- return value ? 1 : 0 unless parameter
106
-
107
- sources = parameter_sources(request, parameter)
108
- sources.size + (value && !sources.include?(value) ? 1 : 0)
109
- end
110
-
111
- # The form-encoded body (§2.2) and the URI query (§2.3) as Rack
112
- # exposes them, for a request object that keeps the two apart.
113
- def parameter_sources(request, parameter)
114
- return [] unless request.respond_to?(:GET) && request.respond_to?(:POST)
115
-
116
- query = request.GET
117
- body = body_parameters(request)
118
- return [] unless query.is_a?(Hash) && body.is_a?(Hash)
119
-
120
- [query[parameter], body[parameter]].filter_map(&:presence)
121
- end
122
-
123
- # A body ActionDispatch cannot parse — malformed JSON under a JSON
124
- # content type, say — carries no token. Reading it must not raise out
125
- # of the extractors, which host apps call from places that run after
126
- # their own ParseError handling (instrumentation, exception apps), the
127
- # same way ActionDispatch's own #filtered_parameters treats that error.
128
- def parameters(request)
129
- request.parameters
130
- rescue ActionDispatch::Http::Parameters::ParseError
131
- {}
132
- end
133
-
134
- def body_parameters(request)
135
- request.POST
136
- rescue ActionDispatch::Http::Parameters::ParseError
137
- {}
138
- end
139
-
140
47
  def token_from_basic_header(header, pattern)
141
48
  encoded_header = token_from_header(header, pattern)
142
49
  decode_basic_credentials_token(encoded_header)
@@ -8,9 +8,13 @@ module Doorkeeper
8
8
  class TokenIntrospection
9
9
  attr_reader :token, :error, :invalid_request_reason
10
10
 
11
- def initialize(server, token)
11
+ # +token_type+ tells which credential of the token record the caller
12
+ # actually received (:access_token or :refresh_token), so the "active"
13
+ # state and the response describe the presented token per RFC 7662 §2.2.
14
+ def initialize(server, token, token_type: :access_token)
12
15
  @server = server
13
16
  @token = token
17
+ @token_type = token_type
14
18
  end
15
19
 
16
20
  def authorized?
@@ -106,12 +110,19 @@ module Doorkeeper
106
110
  active: true,
107
111
  scope: @token.scopes_string,
108
112
  client_id: @token.try(:application).try(:uid),
109
- token_type: @token.token_type,
110
113
  iat: @token.created_at.to_i,
111
114
  }
112
- # `exp` is OPTIONAL per RFC 7662 §2.2; omit it for non-expiring tokens
113
- # so clients don't interpret `0` as "expired at 1970-01-01".
114
- response[:exp] = @token.expires_at.to_i if @token.expires_at
115
+
116
+ # `token_type` (RFC 6749 §7.1) and `exp` describe the access token:
117
+ # a refresh token is not a Bearer credential and has no expiry of its
118
+ # own, so both are omitted (they are OPTIONAL per RFC 7662 §2.2) when
119
+ # a refresh token is presented.
120
+ unless refresh_token_presented?
121
+ response[:token_type] = @token.token_type
122
+ # `exp` is OPTIONAL per RFC 7662 §2.2; omit it for non-expiring tokens
123
+ # so clients don't interpret `0` as "expired at 1970-01-01".
124
+ response[:exp] = @token.expires_at.to_i if @token.expires_at
125
+ end
115
126
 
116
127
  customize_response(response)
117
128
  end
@@ -176,9 +187,18 @@ module Doorkeeper
176
187
  end
177
188
  end
178
189
 
179
- # Token can be valid only if it is not expired or revoked.
190
+ # The presented token can be valid only if it is not revoked; an access
191
+ # token must additionally be unexpired. A refresh token has no expiry of
192
+ # its own and stays usable at the token endpoint after its paired access
193
+ # token expires, so that expiry is not consulted here.
180
194
  def valid_token?
181
- @token&.accessible?
195
+ return false if @token.blank?
196
+
197
+ refresh_token_presented? ? !@token.revoked? : @token.accessible?
198
+ end
199
+
200
+ def refresh_token_presented?
201
+ @token_type == :refresh_token
182
202
  end
183
203
 
184
204
  def valid_authorized_token?
@@ -116,9 +116,9 @@ module Doorkeeper::Orm::ActiveRecord::Mixins
116
116
 
117
117
  return generator if generator.respond_to?(:generate)
118
118
 
119
- raise Errors::UnableToGenerateToken, "#{generator} does not respond to `.generate`."
119
+ raise Doorkeeper::Errors::UnableToGenerateToken, "#{generator} does not respond to `.generate`."
120
120
  rescue NameError
121
- raise Errors::TokenGeneratorNotFound, "#{generator_name} not found"
121
+ raise Doorkeeper::Errors::TokenGeneratorNotFound, "#{generator_name} not found"
122
122
  end
123
123
 
124
124
  def generate_uid
@@ -34,12 +34,8 @@ module Doorkeeper
34
34
  Doorkeeper::OAuth::NonStandard::IETF_WG_OAUTH2_OOB_METHODS.include?(uri)
35
35
  end
36
36
 
37
- # Script schemes are refused before the host application's rule is
38
- # consulted, so a `forbid_redirect_uri` that rejects them too does not
39
- # report the error twice.
40
37
  def forbidden_uri?(uri)
41
- Doorkeeper::OAuth::Helpers::URIChecker.script_scheme?(uri) ||
42
- Doorkeeper.config.forbid_redirect_uri.call(uri)
38
+ Doorkeeper.config.forbid_redirect_uri.call(uri)
43
39
  end
44
40
 
45
41
  def unspecified_scheme?(uri)
@@ -11,6 +11,7 @@ module Doorkeeper
11
11
  authorizations: "doorkeeper/authorizations",
12
12
  applications: "doorkeeper/applications",
13
13
  authorized_applications: "doorkeeper/authorized_applications",
14
+ metadata: "doorkeeper/metadata",
14
15
  tokens: "doorkeeper/tokens",
15
16
  token_info: "doorkeeper/token_info",
16
17
  }
@@ -41,10 +41,16 @@ module Doorkeeper
41
41
  map_route(:authorized_applications, :authorized_applications_routes)
42
42
  map_route(:token_info, :token_info_routes)
43
43
  end
44
+
45
+ map_route(:metadata, :metadata_routes)
44
46
  end
45
47
 
46
48
  private
47
49
 
50
+ def metadata_routes(mapping)
51
+ routes.get ".well-known/oauth-authorization-server", controller: mapping[:controllers], action: :show
52
+ end
53
+
48
54
  def authorization_routes(mapping)
49
55
  routes.resource(
50
56
  :authorization,