doorkeeper 5.1.0.rc1 → 5.1.0.rc2
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 +11 -2
- data/Appraisals +29 -3
- data/Gemfile +13 -5
- data/NEWS.md +52 -15
- data/README.md +68 -487
- data/app/controllers/doorkeeper/token_info_controller.rb +1 -1
- data/app/controllers/doorkeeper/tokens_controller.rb +1 -1
- data/doorkeeper.gemspec +3 -2
- data/gemfiles/rails_4_2.gemfile +8 -5
- data/gemfiles/rails_5_0.gemfile +9 -6
- data/gemfiles/rails_5_1.gemfile +9 -6
- data/gemfiles/rails_5_2.gemfile +9 -6
- data/gemfiles/rails_6_0.gemfile +16 -0
- data/gemfiles/rails_master.gemfile +8 -10
- data/lib/doorkeeper.rb +7 -1
- data/lib/doorkeeper/config.rb +110 -24
- data/lib/doorkeeper/models/access_grant_mixin.rb +15 -7
- data/lib/doorkeeper/models/access_token_mixin.rb +29 -16
- data/lib/doorkeeper/models/application_mixin.rb +18 -28
- data/lib/doorkeeper/models/concerns/expirable.rb +3 -2
- data/lib/doorkeeper/models/concerns/reusable.rb +19 -0
- data/lib/doorkeeper/models/concerns/scopes.rb +4 -0
- data/lib/doorkeeper/models/concerns/secret_storable.rb +106 -0
- data/lib/doorkeeper/oauth/authorization/token.rb +3 -1
- data/lib/doorkeeper/oauth/error_response.rb +5 -1
- data/lib/doorkeeper/oauth/helpers/unique_token.rb +12 -1
- data/lib/doorkeeper/oauth/invalid_token_response.rb +4 -0
- data/lib/doorkeeper/oauth/token_introspection.rb +72 -6
- data/lib/doorkeeper/orm/active_record/access_grant.rb +9 -8
- data/lib/doorkeeper/orm/active_record/application.rb +10 -6
- data/lib/doorkeeper/secret_storing/base.rb +63 -0
- data/lib/doorkeeper/secret_storing/bcrypt.rb +59 -0
- data/lib/doorkeeper/secret_storing/plain.rb +33 -0
- data/lib/doorkeeper/secret_storing/sha256_hash.rb +25 -0
- data/lib/doorkeeper/version.rb +1 -1
- data/lib/generators/doorkeeper/templates/initializer.rb +62 -20
- data/spec/controllers/authorizations_controller_spec.rb +3 -3
- data/spec/controllers/token_info_controller_spec.rb +1 -1
- data/spec/controllers/tokens_controller_spec.rb +78 -30
- data/spec/dummy/config/application.rb +12 -1
- data/spec/lib/config_spec.rb +119 -35
- data/spec/lib/models/expirable_spec.rb +12 -0
- data/spec/lib/models/reusable_spec.rb +40 -0
- data/spec/lib/models/scopes_spec.rb +13 -1
- data/spec/lib/models/secret_storable_spec.rb +113 -0
- data/spec/lib/oauth/authorization_code_request_spec.rb +18 -1
- data/spec/lib/oauth/client_credentials/creator_spec.rb +51 -7
- data/spec/lib/oauth/error_response_spec.rb +7 -1
- data/spec/lib/oauth/password_access_token_request_spec.rb +11 -1
- data/spec/lib/oauth/token_request_spec.rb +16 -1
- data/spec/lib/secret_storing/base_spec.rb +60 -0
- data/spec/lib/secret_storing/bcrypt_spec.rb +49 -0
- data/spec/lib/secret_storing/plain_spec.rb +44 -0
- data/spec/lib/secret_storing/sha256_hash_spec.rb +48 -0
- data/spec/models/doorkeeper/application_spec.rb +23 -4
- data/spec/requests/flows/authorization_code_spec.rb +3 -3
- data/spec/requests/flows/client_credentials_spec.rb +2 -2
- data/spec/requests/flows/implicit_grant_spec.rb +1 -1
- data/spec/requests/flows/password_spec.rb +3 -3
- data/spec/routing/custom_controller_routes_spec.rb +4 -0
- data/spec/support/shared/hashing_shared_context.rb +12 -5
- metadata +51 -21
- data/lib/doorkeeper/models/concerns/hashable.rb +0 -137
- data/spec/lib/models/hashable_spec.rb +0 -183
@@ -1,40 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'bcrypt'
|
4
|
-
|
5
3
|
module Doorkeeper
|
6
4
|
module ApplicationMixin
|
7
5
|
extend ActiveSupport::Concern
|
8
6
|
|
9
7
|
include OAuth::Helpers
|
10
8
|
include Models::Orderable
|
11
|
-
include Models::
|
9
|
+
include Models::SecretStorable
|
12
10
|
include Models::Scopes
|
13
11
|
|
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
12
|
# :nodoc
|
31
13
|
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
|
-
|
38
14
|
# Returns an instance of the Doorkeeper::Application with
|
39
15
|
# specific UID and secret.
|
40
16
|
#
|
@@ -65,6 +41,20 @@ module Doorkeeper
|
|
65
41
|
def by_uid(uid)
|
66
42
|
find_by(uid: uid.to_s)
|
67
43
|
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Determines the secret storing transformer
|
47
|
+
# Unless configured otherwise, uses the plain secret strategy
|
48
|
+
def secret_strategy
|
49
|
+
::Doorkeeper.configuration.application_secret_strategy
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Determine the fallback storing strategy
|
54
|
+
# Unless configured, there will be no fallback
|
55
|
+
def fallback_secret_strategy
|
56
|
+
::Doorkeeper.configuration.application_secret_fallback_strategy
|
57
|
+
end
|
68
58
|
end
|
69
59
|
|
70
60
|
# Set an application's valid redirect URIs.
|
@@ -90,12 +80,12 @@ module Doorkeeper
|
|
90
80
|
return false if input.nil? || secret.nil?
|
91
81
|
|
92
82
|
# When matching the secret by comparer function, all is well.
|
93
|
-
return true if
|
83
|
+
return true if secret_strategy.secret_matches?(input, secret)
|
94
84
|
|
95
85
|
# When fallback lookup is enabled, ensure applications
|
96
86
|
# with plain secrets can still be found
|
97
|
-
if
|
98
|
-
|
87
|
+
if fallback_secret_strategy
|
88
|
+
fallback_secret_strategy.secret_matches?(input, secret)
|
99
89
|
else
|
100
90
|
false
|
101
91
|
end
|
@@ -24,10 +24,11 @@ module Doorkeeper
|
|
24
24
|
|
25
25
|
# Expiration time (date time of creation + TTL).
|
26
26
|
#
|
27
|
-
# @return [Time] expiration time in UTC
|
27
|
+
# @return [Time, nil] expiration time in UTC
|
28
|
+
# or nil if the object never expires.
|
28
29
|
#
|
29
30
|
def expires_at
|
30
|
-
created_at + expires_in.seconds
|
31
|
+
expires_in && created_at + expires_in.seconds
|
31
32
|
end
|
32
33
|
end
|
33
34
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doorkeeper
|
4
|
+
module Models
|
5
|
+
module Reusable
|
6
|
+
# Indicates whether the object is reusable (i.e. It is not expired and
|
7
|
+
# has not crossed reuse_limit).
|
8
|
+
#
|
9
|
+
# @return [Boolean] true if can be reused and false in other case
|
10
|
+
def reusable?
|
11
|
+
return false if expired?
|
12
|
+
return true unless expires_in
|
13
|
+
|
14
|
+
threshold_limit = 100 - Doorkeeper.configuration.token_reuse_limit
|
15
|
+
expires_in_seconds >= threshold_limit * expires_in / 100
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doorkeeper
|
4
|
+
module Models
|
5
|
+
##
|
6
|
+
# Storable finder to provide lookups for input plaintext values which are
|
7
|
+
# mapped to their stored versions (e.g., hashing, encryption) before lookup.
|
8
|
+
module SecretStorable
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
delegate :secret_strategy,
|
12
|
+
:fallback_secret_strategy,
|
13
|
+
to: :class
|
14
|
+
|
15
|
+
# :nodoc
|
16
|
+
module ClassMethods
|
17
|
+
# Compare the given plaintext with the secret
|
18
|
+
#
|
19
|
+
# @param input [String]
|
20
|
+
# The plain input to compare.
|
21
|
+
#
|
22
|
+
# @param secret [String]
|
23
|
+
# The secret value to compare with.
|
24
|
+
#
|
25
|
+
# @return [Boolean]
|
26
|
+
# Whether input matches secret as per the secret strategy
|
27
|
+
#
|
28
|
+
def secret_matches?(input, secret)
|
29
|
+
secret_strategy.secret_matches?(input, secret)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns an instance of the Doorkeeper::AccessToken with
|
33
|
+
# specific token value.
|
34
|
+
#
|
35
|
+
# @param attr [Symbol]
|
36
|
+
# The token attribute we're looking with.
|
37
|
+
#
|
38
|
+
# @param token [#to_s]
|
39
|
+
# token value (any object that responds to `#to_s`)
|
40
|
+
#
|
41
|
+
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
|
42
|
+
# if there is no record with such token
|
43
|
+
#
|
44
|
+
def find_by_plaintext_token(attr, token)
|
45
|
+
token = token.to_s
|
46
|
+
|
47
|
+
find_by(attr => secret_strategy.transform_secret(token)) ||
|
48
|
+
find_by_fallback_token(attr, token)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Allow looking up previously plain tokens as a fallback
|
52
|
+
# IFF a fallback strategy has been defined
|
53
|
+
#
|
54
|
+
# @param attr [Symbol]
|
55
|
+
# The token attribute we're looking with.
|
56
|
+
#
|
57
|
+
# @param plain_secret [#to_s]
|
58
|
+
# plain secret value (any object that responds to `#to_s`)
|
59
|
+
#
|
60
|
+
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
|
61
|
+
# if there is no record with such token
|
62
|
+
#
|
63
|
+
def find_by_fallback_token(attr, plain_secret)
|
64
|
+
return nil unless fallback_secret_strategy
|
65
|
+
|
66
|
+
# Use the previous strategy to look up
|
67
|
+
stored_token = fallback_secret_strategy.transform_secret(plain_secret)
|
68
|
+
find_by(attr => stored_token).tap do |resource|
|
69
|
+
upgrade_fallback_value resource, attr, plain_secret
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Allow implementations in ORMs to replace a plain
|
74
|
+
# value falling back to to avoid it remaining as plain text.
|
75
|
+
#
|
76
|
+
# @param instance
|
77
|
+
# An instance of this model with a plain value token.
|
78
|
+
#
|
79
|
+
# @param attr
|
80
|
+
# The secret attribute name to upgrade.
|
81
|
+
#
|
82
|
+
# @param plain_secret
|
83
|
+
# The plain secret to upgrade.
|
84
|
+
#
|
85
|
+
def upgrade_fallback_value(instance, attr, plain_secret)
|
86
|
+
upgraded = secret_strategy.store_secret(instance, attr, plain_secret)
|
87
|
+
instance.update(attr => upgraded)
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Determines the secret storing transformer
|
92
|
+
# Unless configured otherwise, uses the plain secret strategy
|
93
|
+
def secret_strategy
|
94
|
+
::Doorkeeper::SecretStoring::Plain
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Determine the fallback storing strategy
|
99
|
+
# Unless configured, there will be no fallback
|
100
|
+
def fallback_secret_strategy
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -8,7 +8,9 @@ module Doorkeeper
|
|
8
8
|
|
9
9
|
class << self
|
10
10
|
def build_context(pre_auth_or_oauth_client, grant_type, scopes)
|
11
|
-
oauth_client = if pre_auth_or_oauth_client.respond_to?(:
|
11
|
+
oauth_client = if pre_auth_or_oauth_client.respond_to?(:application)
|
12
|
+
pre_auth_or_oauth_client.application
|
13
|
+
elsif pre_auth_or_oauth_client.respond_to?(:client)
|
12
14
|
pre_auth_or_oauth_client.client
|
13
15
|
else
|
14
16
|
pre_auth_or_oauth_client
|
@@ -5,10 +5,21 @@ module Doorkeeper
|
|
5
5
|
module Helpers
|
6
6
|
module UniqueToken
|
7
7
|
def self.generate(options = {})
|
8
|
-
|
8
|
+
# Access Token value must be 1*VSCHAR or 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
|
9
|
+
#
|
10
|
+
# @see https://tools.ietf.org/html/rfc6749#appendix-A.12
|
11
|
+
# @see https://tools.ietf.org/html/rfc6750#section-2.1
|
12
|
+
#
|
13
|
+
generator_method = options.delete(:generator) || SecureRandom.method(self.generator_method)
|
9
14
|
token_size = options.delete(:size) || 32
|
10
15
|
generator_method.call(token_size)
|
11
16
|
end
|
17
|
+
|
18
|
+
# Generator method for default generator class (SecureRandom)
|
19
|
+
#
|
20
|
+
def self.generator_method
|
21
|
+
Doorkeeper.configuration.default_generator_method
|
22
|
+
end
|
12
23
|
end
|
13
24
|
end
|
14
25
|
end
|
@@ -20,6 +20,16 @@ module Doorkeeper
|
|
20
20
|
@error.blank?
|
21
21
|
end
|
22
22
|
|
23
|
+
def error_response
|
24
|
+
return if @error.blank?
|
25
|
+
|
26
|
+
if @error == :invalid_token
|
27
|
+
OAuth::InvalidTokenResponse.from_access_token(authorized_token)
|
28
|
+
else
|
29
|
+
OAuth::ErrorResponse.new(name: @error)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
23
33
|
def to_json
|
24
34
|
active? ? success_response : failure_response
|
25
35
|
end
|
@@ -37,13 +47,29 @@ module Doorkeeper
|
|
37
47
|
#
|
38
48
|
# @see https://www.oauth.com/oauth2-servers/token-introspection-endpoint/
|
39
49
|
#
|
50
|
+
# To prevent token scanning attacks, the endpoint MUST also require
|
51
|
+
# some form of authorization to access this endpoint, such as client
|
52
|
+
# authentication as described in OAuth 2.0 [RFC6749] or a separate
|
53
|
+
# OAuth 2.0 access token such as the bearer token described in OAuth
|
54
|
+
# 2.0 Bearer Token Usage [RFC6750].
|
55
|
+
#
|
40
56
|
def authorize!
|
41
57
|
# Requested client authorization
|
42
58
|
if server.credentials
|
43
59
|
@error = :invalid_client unless authorized_client
|
44
|
-
|
60
|
+
elsif authorized_token
|
45
61
|
# Requested bearer token authorization
|
46
|
-
|
62
|
+
#
|
63
|
+
# If the protected resource uses an OAuth 2.0 bearer token to authorize
|
64
|
+
# its call to the introspection endpoint and the token used for
|
65
|
+
# authorization does not contain sufficient privileges or is otherwise
|
66
|
+
# invalid for this request, the authorization server responds with an
|
67
|
+
# HTTP 401 code as described in Section 3 of OAuth 2.0 Bearer Token
|
68
|
+
# Usage [RFC6750].
|
69
|
+
#
|
70
|
+
@error = :invalid_token if authorized_token_matches_introspected? || !authorized_token.accessible?
|
71
|
+
else
|
72
|
+
@error = :invalid_request
|
47
73
|
end
|
48
74
|
end
|
49
75
|
|
@@ -60,14 +86,14 @@ module Doorkeeper
|
|
60
86
|
|
61
87
|
# 2.2. Introspection Response
|
62
88
|
def success_response
|
63
|
-
|
89
|
+
customize_response(
|
64
90
|
active: true,
|
65
91
|
scope: @token.scopes_string,
|
66
92
|
client_id: @token.try(:application).try(:uid),
|
67
93
|
token_type: @token.token_type,
|
68
94
|
exp: @token.expires_at.to_i,
|
69
95
|
iat: @token.created_at.to_i
|
70
|
-
|
96
|
+
)
|
71
97
|
end
|
72
98
|
|
73
99
|
# If the introspection call is properly authorized but the token is not
|
@@ -103,6 +129,25 @@ module Doorkeeper
|
|
103
129
|
# * The token expired
|
104
130
|
# * The token was issued to a different client than is making this request
|
105
131
|
#
|
132
|
+
# Since resource servers using token introspection rely on the
|
133
|
+
# authorization server to determine the state of a token, the
|
134
|
+
# authorization server MUST perform all applicable checks against a
|
135
|
+
# token's state. For instance, these tests include the following:
|
136
|
+
#
|
137
|
+
# o If the token can expire, the authorization server MUST determine
|
138
|
+
# whether or not the token has expired.
|
139
|
+
# o If the token can be issued before it is able to be used, the
|
140
|
+
# authorization server MUST determine whether or not a token's valid
|
141
|
+
# period has started yet.
|
142
|
+
# o If the token can be revoked after it was issued, the authorization
|
143
|
+
# server MUST determine whether or not such a revocation has taken
|
144
|
+
# place.
|
145
|
+
# o If the token has been signed, the authorization server MUST
|
146
|
+
# validate the signature.
|
147
|
+
# o If the token can be used only at certain resource servers, the
|
148
|
+
# authorization server MUST determine whether or not the token can
|
149
|
+
# be used at the resource server making the introspection call.
|
150
|
+
#
|
106
151
|
def active?
|
107
152
|
if authorized_client
|
108
153
|
valid_token? && authorized_for_client?
|
@@ -113,18 +158,39 @@ module Doorkeeper
|
|
113
158
|
|
114
159
|
# Token can be valid only if it is not expired or revoked.
|
115
160
|
def valid_token?
|
116
|
-
@token
|
161
|
+
@token && @token.accessible?
|
162
|
+
end
|
163
|
+
|
164
|
+
# RFC7662 Section 2.1
|
165
|
+
def authorized_token_matches_introspected?
|
166
|
+
authorized_token.token == @token.token
|
117
167
|
end
|
118
168
|
|
119
169
|
# If token doesn't belong to some client, then it is public.
|
120
170
|
# Otherwise in it required for token to be connected to the same client.
|
121
171
|
def authorized_for_client?
|
122
|
-
if @token.application
|
172
|
+
if @token.application
|
123
173
|
@token.application == authorized_client.application
|
124
174
|
else
|
125
175
|
true
|
126
176
|
end
|
127
177
|
end
|
178
|
+
|
179
|
+
# Allows to customize introspection response.
|
180
|
+
# Provides context (controller) and token for generating developer-specific
|
181
|
+
# response.
|
182
|
+
#
|
183
|
+
# @see https://tools.ietf.org/html/rfc7662#section-2.2
|
184
|
+
#
|
185
|
+
def customize_response(response)
|
186
|
+
customized_response = Doorkeeper.configuration.custom_introspection_response.call(
|
187
|
+
token,
|
188
|
+
server.context
|
189
|
+
)
|
190
|
+
return response if customized_response.blank?
|
191
|
+
|
192
|
+
response.merge(customized_response)
|
193
|
+
end
|
128
194
|
end
|
129
195
|
end
|
130
196
|
end
|
@@ -27,16 +27,17 @@ module Doorkeeper
|
|
27
27
|
|
28
28
|
before_validation :generate_token, on: :create
|
29
29
|
|
30
|
-
#
|
31
|
-
#
|
32
|
-
# the configuration hasher and may not be available in plaintext.
|
30
|
+
# We keep a volatile copy of the raw token for initial communication
|
31
|
+
# The stored refresh_token may be mapped and not available in cleartext.
|
33
32
|
#
|
34
|
-
#
|
33
|
+
# Some strategies allow restoring stored secrets (e.g. symmetric encryption)
|
34
|
+
# while hashing strategies do not, so you cannot rely on this value
|
35
|
+
# returning a present value for persisted tokens.
|
35
36
|
def plaintext_token
|
36
|
-
if
|
37
|
-
|
37
|
+
if secret_strategy.allows_restoring_secrets?
|
38
|
+
secret_strategy.restore_secret(self, :token)
|
38
39
|
else
|
39
|
-
|
40
|
+
@raw_token
|
40
41
|
end
|
41
42
|
end
|
42
43
|
|
@@ -48,7 +49,7 @@ module Doorkeeper
|
|
48
49
|
#
|
49
50
|
def generate_token
|
50
51
|
@raw_token = UniqueToken.generate
|
51
|
-
self
|
52
|
+
secret_strategy.store_secret(self, :token, @raw_token)
|
52
53
|
end
|
53
54
|
end
|
54
55
|
end
|