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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -22
- data/README.md +1 -0
- data/app/controllers/doorkeeper/authorizations_controller.rb +3 -33
- data/app/controllers/doorkeeper/authorized_applications_controller.rb +0 -23
- data/app/controllers/doorkeeper/metadata_controller.rb +20 -0
- data/app/controllers/doorkeeper/tokens_controller.rb +10 -1
- data/app/views/doorkeeper/authorizations/new.html.erb +6 -0
- data/lib/doorkeeper/client_authentication/credentials.rb +11 -0
- data/lib/doorkeeper/client_authentication/fallback_method.rb +19 -0
- data/lib/doorkeeper/client_authentication/legacy_callable.rb +46 -0
- data/lib/doorkeeper/client_authentication/method.rb +23 -0
- data/lib/doorkeeper/client_authentication/registry.rb +47 -0
- data/lib/doorkeeper/client_authentication.rb +93 -0
- data/lib/doorkeeper/config/option.rb +1 -1
- data/lib/doorkeeper/config/validations.rb +162 -1
- data/lib/doorkeeper/config.rb +114 -15
- data/lib/doorkeeper/errors.rb +1 -0
- data/lib/doorkeeper/models/access_token_mixin.rb +40 -5
- data/lib/doorkeeper/models/application_mixin.rb +14 -6
- data/lib/doorkeeper/models/concerns/secret_storable.rb +10 -1
- data/lib/doorkeeper/oauth/authorization/uri_builder.rb +11 -0
- data/lib/doorkeeper/oauth/authorization_code_request.rb +4 -1
- data/lib/doorkeeper/oauth/client.rb +18 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +53 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_post.rb +29 -0
- data/lib/doorkeeper/oauth/client_authentication/none.rb +32 -0
- data/lib/doorkeeper/oauth/code_response.rb +16 -2
- data/lib/doorkeeper/oauth/error_response.rb +11 -2
- data/lib/doorkeeper/oauth/helpers/uri_checker.rb +0 -14
- data/lib/doorkeeper/oauth/metadata_response.rb +157 -0
- data/lib/doorkeeper/oauth/pre_authorization.rb +12 -32
- data/lib/doorkeeper/oauth/token.rb +2 -95
- data/lib/doorkeeper/oauth/token_introspection.rb +27 -7
- data/lib/doorkeeper/orm/active_record/mixins/application.rb +2 -2
- data/lib/doorkeeper/orm/active_record/redirect_uri_validator.rb +1 -5
- data/lib/doorkeeper/rails/routes/mapping.rb +1 -0
- data/lib/doorkeeper/rails/routes.rb +6 -0
- data/lib/doorkeeper/request.rb +65 -18
- data/lib/doorkeeper/server.rb +5 -2
- data/lib/doorkeeper/version.rb +4 -4
- data/lib/doorkeeper.rb +8 -4
- data/lib/generators/doorkeeper/templates/initializer.rb +67 -18
- metadata +14 -7
- 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
|
|
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
|
|
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(
|
|
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
|
|
26
|
+
request.parameters[:access_token]
|
|
73
27
|
end
|
|
74
28
|
|
|
75
29
|
def from_bearer_param(request)
|
|
76
|
-
parameters
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
#
|
|
114
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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,
|