doorkeeper 5.0.3 → 5.1.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of doorkeeper might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.travis.yml +7 -3
- data/Dangerfile +5 -2
- data/Gemfile +3 -1
- data/NEWS.md +20 -13
- data/README.md +1 -1
- data/app/controllers/doorkeeper/applications_controller.rb +3 -3
- data/app/controllers/doorkeeper/authorized_applications_controller.rb +1 -1
- data/app/controllers/doorkeeper/tokens_controller.rb +6 -6
- data/app/views/doorkeeper/applications/show.html.erb +1 -1
- data/app/views/layouts/doorkeeper/admin.html.erb +5 -3
- data/bin/console +15 -0
- data/gemfiles/rails_4_2.gemfile +1 -0
- data/gemfiles/rails_5_0.gemfile +1 -0
- data/gemfiles/rails_5_1.gemfile +1 -0
- data/gemfiles/rails_5_2.gemfile +2 -1
- data/gemfiles/rails_master.gemfile +1 -0
- data/lib/doorkeeper.rb +1 -0
- data/lib/doorkeeper/config.rb +73 -6
- data/lib/doorkeeper/helpers/controller.rb +3 -2
- data/lib/doorkeeper/models/access_grant_mixin.rb +8 -1
- data/lib/doorkeeper/models/access_token_mixin.rb +40 -9
- data/lib/doorkeeper/models/application_mixin.rb +52 -1
- data/lib/doorkeeper/models/concerns/hashable.rb +137 -0
- data/lib/doorkeeper/models/concerns/scopes.rb +1 -1
- data/lib/doorkeeper/oauth/authorization/code.rb +1 -1
- data/lib/doorkeeper/oauth/authorization/token.rb +1 -1
- data/lib/doorkeeper/oauth/authorization_code_request.rb +1 -1
- data/lib/doorkeeper/oauth/client.rb +1 -1
- data/lib/doorkeeper/oauth/client_credentials/validation.rb +4 -3
- data/lib/doorkeeper/oauth/code_response.rb +2 -2
- data/lib/doorkeeper/oauth/helpers/scope_checker.rb +23 -8
- data/lib/doorkeeper/oauth/helpers/uri_checker.rb +32 -0
- data/lib/doorkeeper/oauth/password_access_token_request.rb +7 -2
- data/lib/doorkeeper/oauth/pre_authorization.rb +8 -3
- data/lib/doorkeeper/oauth/refresh_token_request.rb +4 -1
- data/lib/doorkeeper/oauth/token_response.rb +2 -2
- data/lib/doorkeeper/orm/active_record/access_grant.rb +22 -2
- data/lib/doorkeeper/orm/active_record/application.rb +12 -53
- data/lib/doorkeeper/version.rb +3 -3
- data/lib/generators/doorkeeper/templates/initializer.rb +41 -1
- data/spec/controllers/application_metal_controller_spec.rb +18 -4
- data/spec/controllers/tokens_controller_spec.rb +7 -11
- data/spec/dummy/app/controllers/application_controller.rb +1 -1
- data/spec/factories.rb +3 -3
- data/spec/lib/config_spec.rb +84 -0
- data/spec/lib/models/hashable_spec.rb +183 -0
- data/spec/lib/oauth/base_request_spec.rb +7 -7
- data/spec/lib/oauth/client_credentials/validation_spec.rb +3 -0
- data/spec/lib/oauth/helpers/scope_checker_spec.rb +52 -17
- data/spec/lib/oauth/helpers/uri_checker_spec.rb +20 -2
- data/spec/lib/oauth/password_access_token_request_spec.rb +32 -11
- data/spec/lib/oauth/pre_authorization_spec.rb +24 -0
- data/spec/lib/oauth/token_response_spec.rb +13 -13
- data/spec/lib/oauth/token_spec.rb +14 -0
- data/spec/models/doorkeeper/access_grant_spec.rb +61 -0
- data/spec/models/doorkeeper/access_token_spec.rb +123 -0
- data/spec/models/doorkeeper/application_spec.rb +227 -295
- data/spec/requests/flows/authorization_code_spec.rb +40 -0
- data/spec/requests/flows/password_spec.rb +4 -2
- data/spec/requests/flows/revoke_token_spec.rb +14 -30
- data/spec/spec_helper.rb +2 -1
- data/spec/support/ruby_2_6_rails_4_2_patch.rb +14 -0
- data/spec/support/shared/hashing_shared_context.rb +29 -0
- metadata +12 -4
@@ -55,8 +55,9 @@ module Doorkeeper
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def enforce_content_type
|
58
|
-
|
59
|
-
|
58
|
+
if (request.put? || request.post? || request.patch?) && request.content_type != 'application/x-www-form-urlencoded'
|
59
|
+
render json: {}, status: :unsupported_media_type
|
60
|
+
end
|
60
61
|
end
|
61
62
|
end
|
62
63
|
end
|
@@ -9,6 +9,7 @@ module Doorkeeper
|
|
9
9
|
include Models::Revocable
|
10
10
|
include Models::Accessible
|
11
11
|
include Models::Orderable
|
12
|
+
include Models::Hashable
|
12
13
|
include Models::Scopes
|
13
14
|
|
14
15
|
# never uses pkce, if pkce migrations were not generated
|
@@ -30,7 +31,13 @@ module Doorkeeper
|
|
30
31
|
# if there is no record with such token
|
31
32
|
#
|
32
33
|
def by_token(token)
|
33
|
-
|
34
|
+
find_by_plaintext_token(:token, token)
|
35
|
+
end
|
36
|
+
|
37
|
+
# We want to perform secret hashing whenever the user
|
38
|
+
# enables the configuration option +hash_token_secrets+
|
39
|
+
def perform_secret_hashing?
|
40
|
+
Doorkeeper.configuration.hash_token_secrets?
|
34
41
|
end
|
35
42
|
|
36
43
|
# Revokes AccessGrant records that have not been revoked and associated
|
@@ -9,20 +9,21 @@ module Doorkeeper
|
|
9
9
|
include Models::Revocable
|
10
10
|
include Models::Accessible
|
11
11
|
include Models::Orderable
|
12
|
+
include Models::Hashable
|
12
13
|
include Models::Scopes
|
13
14
|
|
14
15
|
module ClassMethods
|
15
16
|
# Returns an instance of the Doorkeeper::AccessToken with
|
16
|
-
# specific token value.
|
17
|
+
# specific plain text token value.
|
17
18
|
#
|
18
19
|
# @param token [#to_s]
|
19
|
-
# token value (any object that responds to `#to_s`)
|
20
|
+
# Plain text token value (any object that responds to `#to_s`)
|
20
21
|
#
|
21
22
|
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
|
22
23
|
# if there is no record with such token
|
23
24
|
#
|
24
25
|
def by_token(token)
|
25
|
-
|
26
|
+
find_by_plaintext_token(:token, token)
|
26
27
|
end
|
27
28
|
|
28
29
|
# Returns an instance of the Doorkeeper::AccessToken
|
@@ -35,7 +36,13 @@ module Doorkeeper
|
|
35
36
|
# if there is no record with such refresh token
|
36
37
|
#
|
37
38
|
def by_refresh_token(refresh_token)
|
38
|
-
|
39
|
+
find_by_plaintext_token(:refresh_token, refresh_token)
|
40
|
+
end
|
41
|
+
|
42
|
+
# We want to perform secret hashing whenever the user
|
43
|
+
# enables the configuration option +hash_token_secrets+
|
44
|
+
def perform_secret_hashing?
|
45
|
+
Doorkeeper.configuration.hash_token_secrets?
|
39
46
|
end
|
40
47
|
|
41
48
|
# Revokes AccessToken records that have not been revoked and associated
|
@@ -98,9 +105,9 @@ module Doorkeeper
|
|
98
105
|
|
99
106
|
(token_scopes.sort == param_scopes.sort) &&
|
100
107
|
Doorkeeper::OAuth::Helpers::ScopeChecker.valid?(
|
101
|
-
param_scopes.to_s,
|
102
|
-
Doorkeeper.configuration.scopes,
|
103
|
-
app_scopes
|
108
|
+
scope_str: param_scopes.to_s,
|
109
|
+
server_scopes: Doorkeeper.configuration.scopes,
|
110
|
+
app_scopes: app_scopes
|
104
111
|
)
|
105
112
|
end
|
106
113
|
|
@@ -219,6 +226,26 @@ module Doorkeeper
|
|
219
226
|
accessible? && includes_scope?(*scopes)
|
220
227
|
end
|
221
228
|
|
229
|
+
# We keep a volatile copy of the raw refresh token for initial communication
|
230
|
+
# The stored refresh_token may be mapped and not available in cleartext.
|
231
|
+
def plaintext_refresh_token
|
232
|
+
if perform_secret_hashing?
|
233
|
+
@raw_refresh_token
|
234
|
+
else
|
235
|
+
refresh_token
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# We keep a volatile copy of the raw token for initial communication
|
240
|
+
# The stored refresh_token may be mapped and not available in cleartext.
|
241
|
+
def plaintext_token
|
242
|
+
if perform_secret_hashing?
|
243
|
+
@raw_token
|
244
|
+
else
|
245
|
+
token
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
222
249
|
private
|
223
250
|
|
224
251
|
# Generates refresh token with UniqueToken generator.
|
@@ -226,7 +253,8 @@ module Doorkeeper
|
|
226
253
|
# @return [String] refresh token value
|
227
254
|
#
|
228
255
|
def generate_refresh_token
|
229
|
-
|
256
|
+
@raw_refresh_token = UniqueToken.generate
|
257
|
+
self.refresh_token = hashed_or_plain_token(@raw_refresh_token)
|
230
258
|
end
|
231
259
|
|
232
260
|
# Generates and sets the token value with the
|
@@ -242,13 +270,16 @@ module Doorkeeper
|
|
242
270
|
def generate_token
|
243
271
|
self.created_at ||= Time.now.utc
|
244
272
|
|
245
|
-
|
273
|
+
@raw_token = token_generator.generate(
|
246
274
|
resource_owner_id: resource_owner_id,
|
247
275
|
scopes: scopes,
|
248
276
|
application: application,
|
249
277
|
expires_in: expires_in,
|
250
278
|
created_at: created_at
|
251
279
|
)
|
280
|
+
|
281
|
+
self.token = hashed_or_plain_token(@raw_token)
|
282
|
+
@raw_token
|
252
283
|
end
|
253
284
|
|
254
285
|
def token_generator
|
@@ -1,14 +1,40 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'bcrypt'
|
4
|
+
|
3
5
|
module Doorkeeper
|
4
6
|
module ApplicationMixin
|
5
7
|
extend ActiveSupport::Concern
|
6
8
|
|
7
9
|
include OAuth::Helpers
|
8
10
|
include Models::Orderable
|
11
|
+
include Models::Hashable
|
9
12
|
include Models::Scopes
|
10
13
|
|
14
|
+
included do
|
15
|
+
# Use BCrypt as the hashing function for applications
|
16
|
+
self.secret_hash_function = lambda do |plain_token|
|
17
|
+
BCrypt::Password.create(plain_token.to_s)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Also need to override the comparer function for BCrypt
|
21
|
+
self.secret_comparer = lambda do |plain, secret|
|
22
|
+
begin
|
23
|
+
BCrypt::Password.new(secret.to_s) == plain.to_s
|
24
|
+
rescue BCrypt::Errors::InvalidHash
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# :nodoc
|
11
31
|
module ClassMethods
|
32
|
+
# We want to perform secret hashing whenever the user
|
33
|
+
# enables the configuration option +hash_application_secrets+
|
34
|
+
def perform_secret_hashing?
|
35
|
+
Doorkeeper.configuration.hash_application_secrets?
|
36
|
+
end
|
37
|
+
|
12
38
|
# Returns an instance of the Doorkeeper::Application with
|
13
39
|
# specific UID and secret.
|
14
40
|
#
|
@@ -25,7 +51,7 @@ module Doorkeeper
|
|
25
51
|
app = by_uid(uid)
|
26
52
|
return unless app
|
27
53
|
return app if secret.blank? && !app.confidential?
|
28
|
-
return unless app.secret
|
54
|
+
return unless app.secret_matches?(secret)
|
29
55
|
app
|
30
56
|
end
|
31
57
|
|
@@ -49,5 +75,30 @@ module Doorkeeper
|
|
49
75
|
def redirect_uri=(uris)
|
50
76
|
super(uris.is_a?(Array) ? uris.join("\n") : uris)
|
51
77
|
end
|
78
|
+
|
79
|
+
# Check whether the given plain text secret matches our stored secret
|
80
|
+
#
|
81
|
+
# @param input [#to_s] Plain secret provided by user
|
82
|
+
# (any object that responds to `#to_s`)
|
83
|
+
#
|
84
|
+
# @return [true] Whether the given secret matches the stored secret
|
85
|
+
# of this application.
|
86
|
+
#
|
87
|
+
def secret_matches?(input)
|
88
|
+
# return false if either is nil, since secure_compare depends on strings
|
89
|
+
# but Application secrets MAY be nil depending on confidentiality.
|
90
|
+
return false if input.nil? || secret.nil?
|
91
|
+
|
92
|
+
# When matching the secret by comparer function, all is well.
|
93
|
+
return true if self.class.secret_matches?(input, secret)
|
94
|
+
|
95
|
+
# When fallback lookup is enabled, ensure applications
|
96
|
+
# with plain secrets can still be found
|
97
|
+
if Doorkeeper.configuration.fallback_to_plain_secrets?
|
98
|
+
ActiveSupport::SecurityUtils.secure_compare(input, secret)
|
99
|
+
else
|
100
|
+
false
|
101
|
+
end
|
102
|
+
end
|
52
103
|
end
|
53
104
|
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doorkeeper
|
4
|
+
module Models
|
5
|
+
##
|
6
|
+
# Hashable finder to provide lookups for input plaintext values which are
|
7
|
+
# mapped to their hashes before lookup.
|
8
|
+
module Hashable
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
delegate :perform_secret_hashing?,
|
12
|
+
:hashed_or_plain_token,
|
13
|
+
to: :class
|
14
|
+
|
15
|
+
# :nodoc
|
16
|
+
module ClassMethods
|
17
|
+
# Allow to override the hashing method by the including module
|
18
|
+
attr_accessor :secret_hash_function, :secret_comparer
|
19
|
+
|
20
|
+
# Compare the given plaintext with the secret
|
21
|
+
#
|
22
|
+
# @param input [String]
|
23
|
+
# The plain input to compare.
|
24
|
+
#
|
25
|
+
# @param secret [String]
|
26
|
+
# The secret value to compare with.
|
27
|
+
#
|
28
|
+
# @return [Boolean]
|
29
|
+
# If hashing is enabled: Whether the secret equals hashed input
|
30
|
+
# If hashing is disabled: Whether input matches secret
|
31
|
+
#
|
32
|
+
def secret_matches?(input, secret)
|
33
|
+
unless perform_secret_hashing?
|
34
|
+
return ActiveSupport::SecurityUtils.secure_compare input, secret
|
35
|
+
end
|
36
|
+
|
37
|
+
(secret_comparer || method(:default_comparer))
|
38
|
+
.call(input, secret)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns an instance of the Doorkeeper::AccessToken with
|
42
|
+
# specific token value.
|
43
|
+
#
|
44
|
+
# @param attr [Symbol]
|
45
|
+
# The token attribute we're looking with.
|
46
|
+
#
|
47
|
+
# @param token [#to_s]
|
48
|
+
# token value (any object that responds to `#to_s`)
|
49
|
+
#
|
50
|
+
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
|
51
|
+
# if there is no record with such token
|
52
|
+
#
|
53
|
+
def find_by_plaintext_token(attr, token)
|
54
|
+
token = token.to_s
|
55
|
+
|
56
|
+
find_by(attr => hashed_or_plain_token(token)) ||
|
57
|
+
find_by_fallback_token(attr, token)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Allow looking up previously plain tokens as a fallback
|
61
|
+
# IFF respective options are enabled
|
62
|
+
#
|
63
|
+
# @param attr [Symbol]
|
64
|
+
# The token attribute we're looking with.
|
65
|
+
#
|
66
|
+
# @param token [#to_s]
|
67
|
+
# token value (any object that responds to `#to_s`)
|
68
|
+
#
|
69
|
+
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
|
70
|
+
# if there is no record with such token
|
71
|
+
#
|
72
|
+
def find_by_fallback_token(attr, token)
|
73
|
+
return nil unless perform_secret_hashing?
|
74
|
+
return nil unless Doorkeeper.configuration.fallback_to_plain_secrets?
|
75
|
+
|
76
|
+
find_by(attr => token).tap do |fallback|
|
77
|
+
upgrade_fallback_value fallback, attr
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Hash the given input token
|
82
|
+
#
|
83
|
+
# @param plain_token [String]
|
84
|
+
# The plain text token to hash.
|
85
|
+
#
|
86
|
+
# @return [String]
|
87
|
+
# IFF secret hashing enabled, the hashed token,
|
88
|
+
# otherwise returns the plain token.
|
89
|
+
def hashed_or_plain_token(plain_token)
|
90
|
+
if perform_secret_hashing?
|
91
|
+
(secret_hash_function || method(:default_hash_function))
|
92
|
+
.call plain_token
|
93
|
+
else
|
94
|
+
plain_token
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Allow implementations in ORMs to replace a plain
|
99
|
+
# value falling back to to avoid it remaining as plain text.
|
100
|
+
#
|
101
|
+
# @param instance
|
102
|
+
# An instance of this model with a plain value token.
|
103
|
+
#
|
104
|
+
# @param attr
|
105
|
+
# The token attribute to upgrade
|
106
|
+
#
|
107
|
+
def upgrade_fallback_value(instance, attr)
|
108
|
+
plain_token = instance.public_send attr
|
109
|
+
instance.update_column(attr, hashed_or_plain_token(plain_token))
|
110
|
+
end
|
111
|
+
|
112
|
+
# Including classes can override this function to
|
113
|
+
# disable or enable secret hashing dynamically
|
114
|
+
def perform_secret_hashing?
|
115
|
+
true
|
116
|
+
end
|
117
|
+
|
118
|
+
# Return a default hashing function to be used when including
|
119
|
+
# module or user does not specify what to use
|
120
|
+
# @param plain_token [String]
|
121
|
+
# The plain text token to hash.
|
122
|
+
#
|
123
|
+
# @return [String] Hashed plain text token
|
124
|
+
#
|
125
|
+
def default_hash_function(plain_token)
|
126
|
+
::Digest::SHA256.hexdigest plain_token
|
127
|
+
end
|
128
|
+
|
129
|
+
# Return a default comparer for the given hash function
|
130
|
+
def default_comparer(plain, secret)
|
131
|
+
hashed = hashed_or_plain_token(plain)
|
132
|
+
ActiveSupport::SecurityUtils.secure_compare hashed, secret
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -18,7 +18,7 @@ module Doorkeeper
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def self.authenticate(credentials, method = Application.method(:by_uid_and_secret))
|
21
|
-
return
|
21
|
+
return if credentials.blank?
|
22
22
|
|
23
23
|
if (application = method.call(credentials.uid, credentials.secret))
|
24
24
|
new(application)
|
@@ -34,9 +34,10 @@ module Doorkeeper
|
|
34
34
|
end
|
35
35
|
|
36
36
|
ScopeChecker.valid?(
|
37
|
-
@request.scopes.to_s,
|
38
|
-
@server.scopes,
|
39
|
-
application_scopes
|
37
|
+
scope_str: @request.scopes.to_s,
|
38
|
+
server_scopes: @server.scopes,
|
39
|
+
app_scopes: application_scopes,
|
40
|
+
grant_type: Doorkeeper::OAuth::CLIENT_CREDENTIALS
|
40
41
|
)
|
41
42
|
end
|
42
43
|
end
|
@@ -23,7 +23,7 @@ module Doorkeeper
|
|
23
23
|
elsif response_on_fragment
|
24
24
|
Authorization::URIBuilder.uri_with_fragment(
|
25
25
|
pre_auth.redirect_uri,
|
26
|
-
access_token: auth.token.
|
26
|
+
access_token: auth.token.plaintext_token,
|
27
27
|
token_type: auth.token.token_type,
|
28
28
|
expires_in: auth.token.expires_in_seconds,
|
29
29
|
state: pre_auth.state
|
@@ -31,7 +31,7 @@ module Doorkeeper
|
|
31
31
|
else
|
32
32
|
Authorization::URIBuilder.uri_with_query(
|
33
33
|
pre_auth.redirect_uri,
|
34
|
-
code: auth.token.
|
34
|
+
code: auth.token.plaintext_token,
|
35
35
|
state: pre_auth.state
|
36
36
|
)
|
37
37
|
end
|
@@ -7,31 +7,46 @@ module Doorkeeper
|
|
7
7
|
class Validator
|
8
8
|
attr_reader :parsed_scopes, :scope_str
|
9
9
|
|
10
|
-
def initialize(scope_str, server_scopes,
|
10
|
+
def initialize(scope_str, server_scopes, app_scopes, grant_type)
|
11
11
|
@parsed_scopes = OAuth::Scopes.from_string(scope_str)
|
12
12
|
@scope_str = scope_str
|
13
|
-
@valid_scopes = valid_scopes(server_scopes,
|
13
|
+
@valid_scopes = valid_scopes(server_scopes, app_scopes)
|
14
|
+
|
15
|
+
if grant_type
|
16
|
+
@scopes_by_grant_type = Doorkeeper.configuration.scopes_by_grant_type[grant_type.to_sym]
|
17
|
+
end
|
14
18
|
end
|
15
19
|
|
16
20
|
def valid?
|
17
21
|
scope_str.present? &&
|
18
22
|
scope_str !~ /[\n\r\t]/ &&
|
19
|
-
@valid_scopes.has_scopes?(parsed_scopes)
|
23
|
+
@valid_scopes.has_scopes?(parsed_scopes) &&
|
24
|
+
permitted_to_grant_type?
|
20
25
|
end
|
21
26
|
|
22
27
|
private
|
23
28
|
|
24
|
-
def valid_scopes(server_scopes,
|
25
|
-
if
|
26
|
-
|
29
|
+
def valid_scopes(server_scopes, app_scopes)
|
30
|
+
if app_scopes.present?
|
31
|
+
app_scopes
|
27
32
|
else
|
28
33
|
server_scopes
|
29
34
|
end
|
30
35
|
end
|
36
|
+
|
37
|
+
def permitted_to_grant_type?
|
38
|
+
return true unless @scopes_by_grant_type
|
39
|
+
|
40
|
+
OAuth::Scopes.from_array(@scopes_by_grant_type)
|
41
|
+
.has_scopes?(parsed_scopes)
|
42
|
+
end
|
31
43
|
end
|
32
44
|
|
33
|
-
def self.valid?(scope_str
|
34
|
-
Validator.new(scope_str,
|
45
|
+
def self.valid?(scope_str:, server_scopes:, app_scopes: nil, grant_type: nil)
|
46
|
+
Validator.new(scope_str,
|
47
|
+
server_scopes,
|
48
|
+
app_scopes,
|
49
|
+
grant_type).valid?
|
35
50
|
end
|
36
51
|
end
|
37
52
|
end
|