doorkeeper 5.9.7 → 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 -12
- data/README.md +1 -0
- 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 +113 -12
- 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/metadata_response.rb +157 -0
- data/lib/doorkeeper/oauth/pre_authorization.rb +12 -5
- data/lib/doorkeeper/oauth/token.rb +0 -76
- data/lib/doorkeeper/oauth/token_introspection.rb +27 -7
- data/lib/doorkeeper/orm/active_record/mixins/application.rb +2 -2
- data/lib/doorkeeper/rails/routes/mapping.rb +1 -0
- data/lib/doorkeeper/rails/routes.rb +6 -0
- data/lib/doorkeeper/request.rb +59 -0
- 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 +65 -13
- metadata +14 -7
- data/lib/doorkeeper/oauth/client/credentials.rb +0 -86
|
@@ -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
|
|
@@ -7,9 +7,9 @@ module Doorkeeper
|
|
|
7
7
|
|
|
8
8
|
validate :client_id, error: Errors::InvalidRequest
|
|
9
9
|
validate :client, error: Errors::InvalidClient
|
|
10
|
-
validate :client_supports_grant_flow, error: Errors::UnauthorizedClient
|
|
11
|
-
validate :resource_owner_authorize_for_client, error: Errors::InvalidClient
|
|
12
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
|
|
13
13
|
validate :params, error: Errors::InvalidRequest
|
|
14
14
|
validate :response_type, error: Errors::UnsupportedResponseType
|
|
15
15
|
validate :response_mode, error: Errors::UnsupportedResponseMode
|
|
@@ -49,13 +49,21 @@ module Doorkeeper
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
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.
|
|
52
55
|
if error == Errors::InvalidRequest
|
|
53
56
|
OAuth::InvalidRequestResponse.from_request(
|
|
54
57
|
self,
|
|
55
58
|
response_on_fragment: response_on_fragment?,
|
|
59
|
+
issuer: Doorkeeper.config.issuer,
|
|
56
60
|
)
|
|
57
61
|
else
|
|
58
|
-
OAuth::ErrorResponse.from_request(
|
|
62
|
+
OAuth::ErrorResponse.from_request(
|
|
63
|
+
self,
|
|
64
|
+
response_on_fragment: response_on_fragment?,
|
|
65
|
+
issuer: Doorkeeper.config.issuer,
|
|
66
|
+
)
|
|
59
67
|
end
|
|
60
68
|
end
|
|
61
69
|
|
|
@@ -147,7 +155,6 @@ module Doorkeeper
|
|
|
147
155
|
|
|
148
156
|
def validate_code_challenge
|
|
149
157
|
return true unless Doorkeeper.config.force_pkce?
|
|
150
|
-
return true if client.confidential
|
|
151
158
|
return true if code_challenge.present?
|
|
152
159
|
|
|
153
160
|
@invalid_request_reason = :invalid_code_challenge
|
|
@@ -180,7 +187,7 @@ module Doorkeeper
|
|
|
180
187
|
scope: scope,
|
|
181
188
|
client_name: client.name,
|
|
182
189
|
status: I18n.t("doorkeeper.pre_authorization.status"),
|
|
183
|
-
}
|
|
190
|
+
}.reverse_merge(custom_access_token_attributes.symbolize_keys)
|
|
184
191
|
end
|
|
185
192
|
end
|
|
186
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)
|
|
@@ -90,36 +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 = request.POST
|
|
118
|
-
return [] unless query.is_a?(Hash) && body.is_a?(Hash)
|
|
119
|
-
|
|
120
|
-
[query[parameter], body[parameter]].filter_map(&:presence)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
47
|
def token_from_basic_header(header, pattern)
|
|
124
48
|
encoded_header = token_from_header(header, pattern)
|
|
125
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
|
|
@@ -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,
|
data/lib/doorkeeper/request.rb
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
module Doorkeeper
|
|
4
4
|
module Request
|
|
5
5
|
class << self
|
|
6
|
+
# Detect the OAuth client authentication method (RFC 6749 §2.3) that the
|
|
7
|
+
# given request uses. Returns the matching method's strategy (not the
|
|
8
|
+
# registry's Method wrapper), or FallbackMethod when none matches
|
|
9
|
+
# (which authenticates to no credentials).
|
|
10
|
+
#
|
|
11
|
+
# Raises Errors::MultipleClientAuthMethods when the request itself
|
|
12
|
+
# uses more than one client authentication method, since RFC 6749 §2.3
|
|
13
|
+
# forbids that (see +validate_client_authentication!+).
|
|
14
|
+
def client_authentication_method(request)
|
|
15
|
+
validate_client_authentication!(request)
|
|
16
|
+
|
|
17
|
+
authentication_method = client_authentication_methods.detect do |method|
|
|
18
|
+
method.matches_request?(request)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if authentication_method
|
|
22
|
+
authentication_method.strategy
|
|
23
|
+
else
|
|
24
|
+
Doorkeeper::ClientAuthentication::FallbackMethod
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
6
28
|
def authorization_strategy(response_type)
|
|
7
29
|
grant_flow = authorization_flows.detect do |flow|
|
|
8
30
|
flow.matches_response_type?(response_type)
|
|
@@ -40,6 +62,43 @@ module Doorkeeper
|
|
|
40
62
|
|
|
41
63
|
private
|
|
42
64
|
|
|
65
|
+
# RFC 6749 §2.3 forbids clients to "use more than one authentication
|
|
66
|
+
# method in each request", so the request payload is validated against
|
|
67
|
+
# every *registered* method — regardless of which ones are configured —
|
|
68
|
+
# before any method is selected: a client sending, say, both Basic
|
|
69
|
+
# credentials and body credentials is rejected even when only one of
|
|
70
|
+
# those methods is enabled on the server.
|
|
71
|
+
#
|
|
72
|
+
# Only real authentication mechanisms count towards the limit:
|
|
73
|
+
#
|
|
74
|
+
# * +:none+ is the absence of client authentication (a public client
|
|
75
|
+
# identifying itself with a bare +client_id+), not a mechanism of its
|
|
76
|
+
# own — RFC 7521 §4.2, for example, explicitly allows a +client_id+
|
|
77
|
+
# next to a client assertion.
|
|
78
|
+
# * Deprecated +client_credentials+ callable extractors are
|
|
79
|
+
# configuration adapters rather than registered methods, so they
|
|
80
|
+
# cannot count either; they keep the historical "first extractor that
|
|
81
|
+
# returns a uid wins" selection (see
|
|
82
|
+
# ClientAuthentication::LegacyCallable) for the deprecation window.
|
|
83
|
+
def validate_client_authentication!(request)
|
|
84
|
+
matched = 0
|
|
85
|
+
|
|
86
|
+
Doorkeeper::ClientAuthentication.registered_methods.each_value do |method|
|
|
87
|
+
next if method.name == :none
|
|
88
|
+
next unless method.matches_request?(request)
|
|
89
|
+
|
|
90
|
+
matched += 1
|
|
91
|
+
|
|
92
|
+
# RFC 6749 §2.3 only forbids using more than one method, so bail out
|
|
93
|
+
# on the second match instead of evaluating the remaining methods.
|
|
94
|
+
raise Errors::MultipleClientAuthMethods if matched > 1
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def client_authentication_methods
|
|
99
|
+
Doorkeeper.configuration.client_authentication_methods
|
|
100
|
+
end
|
|
101
|
+
|
|
43
102
|
def authorization_flows
|
|
44
103
|
Doorkeeper.configuration.authorization_response_flows
|
|
45
104
|
end
|
data/lib/doorkeeper/server.rb
CHANGED
|
@@ -8,6 +8,10 @@ module Doorkeeper
|
|
|
8
8
|
@context = context
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
def client_authentication_method_for_request
|
|
12
|
+
Request.client_authentication_method(context.request)
|
|
13
|
+
end
|
|
14
|
+
|
|
11
15
|
def authorization_request(strategy)
|
|
12
16
|
klass = Request.authorization_strategy(strategy)
|
|
13
17
|
klass.new(self)
|
|
@@ -37,8 +41,7 @@ module Doorkeeper
|
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
def credentials
|
|
40
|
-
|
|
41
|
-
@credentials ||= OAuth::Client::Credentials.from_request(context.request, *methods)
|
|
44
|
+
@credentials ||= client_authentication_method_for_request.authenticate(context.request)
|
|
42
45
|
end
|
|
43
46
|
end
|
|
44
47
|
end
|
data/lib/doorkeeper/version.rb
CHANGED
data/lib/doorkeeper.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "doorkeeper/engine"
|
|
|
7
7
|
#
|
|
8
8
|
module Doorkeeper
|
|
9
9
|
autoload :Errors, "doorkeeper/errors"
|
|
10
|
+
autoload :ClientAuthentication, "doorkeeper/client_authentication"
|
|
10
11
|
autoload :GrantFlow, "doorkeeper/grant_flow"
|
|
11
12
|
autoload :OAuth, "doorkeeper/oauth"
|
|
12
13
|
autoload :Rake, "doorkeeper/rake"
|
|
@@ -52,6 +53,7 @@ module Doorkeeper
|
|
|
52
53
|
autoload :InvalidTokenResponse, "doorkeeper/oauth/invalid_token_response"
|
|
53
54
|
autoload :InvalidRequestResponse, "doorkeeper/oauth/invalid_request_response"
|
|
54
55
|
autoload :ForbiddenTokenResponse, "doorkeeper/oauth/forbidden_token_response"
|
|
56
|
+
autoload :MetadataResponse, "doorkeeper/oauth/metadata_response"
|
|
55
57
|
autoload :NonStandard, "doorkeeper/oauth/nonstandard"
|
|
56
58
|
autoload :PasswordAccessTokenRequest, "doorkeeper/oauth/password_access_token_request"
|
|
57
59
|
autoload :PreAuthorization, "doorkeeper/oauth/pre_authorization"
|
|
@@ -62,6 +64,12 @@ module Doorkeeper
|
|
|
62
64
|
autoload :TokenRequest, "doorkeeper/oauth/token_request"
|
|
63
65
|
autoload :TokenResponse, "doorkeeper/oauth/token_response"
|
|
64
66
|
|
|
67
|
+
module ClientAuthentication
|
|
68
|
+
autoload :None, "doorkeeper/oauth/client_authentication/none"
|
|
69
|
+
autoload :ClientSecretBasic, "doorkeeper/oauth/client_authentication/client_secret_basic"
|
|
70
|
+
autoload :ClientSecretPost, "doorkeeper/oauth/client_authentication/client_secret_post"
|
|
71
|
+
end
|
|
72
|
+
|
|
65
73
|
module Authorization
|
|
66
74
|
autoload :Code, "doorkeeper/oauth/authorization/code"
|
|
67
75
|
autoload :Context, "doorkeeper/oauth/authorization/context"
|
|
@@ -69,10 +77,6 @@ module Doorkeeper
|
|
|
69
77
|
autoload :URIBuilder, "doorkeeper/oauth/authorization/uri_builder"
|
|
70
78
|
end
|
|
71
79
|
|
|
72
|
-
class Client
|
|
73
|
-
autoload :Credentials, "doorkeeper/oauth/client/credentials"
|
|
74
|
-
end
|
|
75
|
-
|
|
76
80
|
module ClientCredentials
|
|
77
81
|
autoload :Validator, "doorkeeper/oauth/client_credentials/validator"
|
|
78
82
|
autoload :Creator, "doorkeeper/oauth/client_credentials/creator"
|
|
@@ -150,6 +150,15 @@ Doorkeeper.configure do
|
|
|
150
150
|
# token found for the application, resources owner and/or set of scopes.
|
|
151
151
|
# Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383
|
|
152
152
|
#
|
|
153
|
+
# Matching considers only the application, resource owner, scopes, custom access token
|
|
154
|
+
# attributes (see +custom_access_token_attributes+) and whether a refresh token is
|
|
155
|
+
# expected — not the authorization request that produced the token. Separate authorization
|
|
156
|
+
# grants for the same combination share a single access token, so concurrent sessions of
|
|
157
|
+
# the same client become interdependent (e.g. refreshing the token in one session revokes
|
|
158
|
+
# it for the others). If you need independent tokens per session or device, keep this
|
|
159
|
+
# option disabled or differentiate the sessions with +custom_access_token_attributes+.
|
|
160
|
+
# See https://github.com/doorkeeper-gem/doorkeeper/issues/1693
|
|
161
|
+
#
|
|
153
162
|
# You can not enable this option together with +hash_token_secrets+.
|
|
154
163
|
#
|
|
155
164
|
# reuse_access_token
|
|
@@ -188,8 +197,8 @@ Doorkeeper.configure do
|
|
|
188
197
|
#
|
|
189
198
|
# revoke_previous_authorization_code_token
|
|
190
199
|
|
|
191
|
-
# Require
|
|
192
|
-
# to obtain an access_token (disabled by default)
|
|
200
|
+
# Require all clients (including confidential ones) to use PKCE when using an
|
|
201
|
+
# authorization code to obtain an access_token (disabled by default)
|
|
193
202
|
#
|
|
194
203
|
# force_pkce
|
|
195
204
|
|
|
@@ -277,21 +286,18 @@ Doorkeeper.configure do
|
|
|
277
286
|
#
|
|
278
287
|
# enforce_configured_scopes
|
|
279
288
|
|
|
280
|
-
#
|
|
281
|
-
#
|
|
282
|
-
#
|
|
289
|
+
# Configure the OAuth client authentication methods (RFC 6749 §2.3) Doorkeeper
|
|
290
|
+
# will accept and the order in which they are tried. By default it accepts
|
|
291
|
+
# HTTP Basic auth (`client_secret_basic`), credentials in the request body
|
|
292
|
+
# (`client_secret_post`), and public clients with no secret (`none`).
|
|
283
293
|
# Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated
|
|
284
294
|
# for more information on customization
|
|
285
295
|
#
|
|
286
|
-
#
|
|
287
|
-
# §2.3 checks that reject a request authenticating the client with more than
|
|
288
|
-
# one method, or presenting more than one client identity. A callable may
|
|
289
|
-
# legitimately overlap the built-in extractors, and the extractors after the
|
|
290
|
-
# matching one are never called, so what they would have returned is unknown.
|
|
291
|
-
# Prefer the symbol extractors unless you need something they cannot express;
|
|
292
|
-
# a callable has to reject such a request itself.
|
|
296
|
+
# client_authentication %i[client_secret_basic client_secret_post none]
|
|
293
297
|
#
|
|
294
|
-
# client_credentials
|
|
298
|
+
# The legacy `client_credentials` option is deprecated; `:from_basic` and
|
|
299
|
+
# `:from_params` are automatically mapped to `:client_secret_basic` and
|
|
300
|
+
# `:client_secret_post`.
|
|
295
301
|
|
|
296
302
|
# Change the way access token is authenticated from the request object.
|
|
297
303
|
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
|
@@ -385,6 +391,9 @@ Doorkeeper.configure do
|
|
|
385
391
|
# If not specified, Doorkeeper enables authorization_code and
|
|
386
392
|
# client_credentials.
|
|
387
393
|
#
|
|
394
|
+
# The Refresh Token Grant Flow ("refresh_token") doesn't need to be listed
|
|
395
|
+
# here: it is enabled automatically when +use_refresh_token+ is configured.
|
|
396
|
+
#
|
|
388
397
|
# implicit and password grant flows have risks that you should understand
|
|
389
398
|
# before enabling:
|
|
390
399
|
# https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.2
|
|
@@ -549,4 +558,47 @@ Doorkeeper.configure do
|
|
|
549
558
|
# WWW-Authenticate Realm (default: "Doorkeeper").
|
|
550
559
|
#
|
|
551
560
|
# realm "Doorkeeper"
|
|
561
|
+
|
|
562
|
+
# OAuth 2.0 Authorization Server Metadata (RFC 8414).
|
|
563
|
+
#
|
|
564
|
+
# Doorkeeper exposes an authorization server metadata document at
|
|
565
|
+
# `/.well-known/oauth-authorization-server`, built from the configuration
|
|
566
|
+
# above. The two options below let you customize that document.
|
|
567
|
+
#
|
|
568
|
+
# `issuer` is the authorization server's issuer identifier. When left as nil
|
|
569
|
+
# (the default) the request base URL is used for the metadata `issuer` field
|
|
570
|
+
# only. Note the asymmetry: RFC 9207 below is gated on an explicitly
|
|
571
|
+
# configured issuer, so leaving it nil still advertises a metadata `issuer`
|
|
572
|
+
# (the base URL) while `authorization_response_iss_parameter_supported` stays
|
|
573
|
+
# false and no `iss` parameter is emitted.
|
|
574
|
+
#
|
|
575
|
+
# Configuring an issuer also enables RFC 9207 (Authorization Server Issuer
|
|
576
|
+
# Identification): the `iss` parameter is added to the authorization
|
|
577
|
+
# responses redirected back to the client - both successful responses and
|
|
578
|
+
# error responses such as access_denied - and advertised via the
|
|
579
|
+
# `authorization_response_iss_parameter_supported` metadata field. Clients
|
|
580
|
+
# that parse the authorization redirect will start seeing this parameter.
|
|
581
|
+
# Per RFC 8414 and RFC 9207 the value should be an https URL with no query or
|
|
582
|
+
# fragment; a non-compliant value logs a warning at boot but is still used.
|
|
583
|
+
# Prefer a host-only issuer: Doorkeeper serves its metadata only at the root
|
|
584
|
+
# /.well-known/oauth-authorization-server, so a path-bearing issuer (e.g.
|
|
585
|
+
# https://auth.example.com/tenant) is not discoverable by RFC 8414 clients and
|
|
586
|
+
# also logs a warning.
|
|
587
|
+
#
|
|
588
|
+
# issuer "https://auth.example.com"
|
|
589
|
+
#
|
|
590
|
+
# `custom_metadata` is a Hash that is merged into the metadata response. Use
|
|
591
|
+
# it to advertise additional or non-default metadata fields, for example a
|
|
592
|
+
# `userinfo_endpoint` (which Doorkeeper itself leaves null) when pairing with
|
|
593
|
+
# an OpenID Connect extension.
|
|
594
|
+
#
|
|
595
|
+
# The merge happens last, so it can also override computed fields - including
|
|
596
|
+
# `authorization_response_iss_parameter_supported`, which Doorkeeper derives
|
|
597
|
+
# from whether `issuer` is set. Overriding a computed field is allowed but is
|
|
598
|
+
# your responsibility to keep consistent with the server's actual behaviour
|
|
599
|
+
# (e.g. don't advertise iss support as true if no `issuer` is configured).
|
|
600
|
+
#
|
|
601
|
+
# custom_metadata(
|
|
602
|
+
# userinfo_endpoint: "https://auth.example.com/oauth/userinfo",
|
|
603
|
+
# )
|
|
552
604
|
end
|