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
@@ -88,19 +88,40 @@ module Doorkeeper::OAuth
|
|
88
88
|
PasswordAccessTokenRequest.new(server, client, owner, scope: 'public')
|
89
89
|
end
|
90
90
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
91
|
+
context 'when scopes_by_grant_type is not configured for grant_type' do
|
92
|
+
it 'returns error when scopes are invalid' do
|
93
|
+
allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string('another'))
|
94
|
+
subject.validate
|
95
|
+
expect(subject.error).to eq(:invalid_scope)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'creates the token with scopes if scopes are valid' do
|
99
|
+
allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string('public'))
|
100
|
+
expect do
|
101
|
+
subject.authorize
|
102
|
+
end.to change { Doorkeeper::AccessToken.count }.by(1)
|
103
|
+
|
104
|
+
expect(Doorkeeper::AccessToken.last.scopes).to include('public')
|
105
|
+
end
|
95
106
|
end
|
96
107
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
108
|
+
context 'when scopes_by_grant_type is configured for grant_type' do
|
109
|
+
it 'returns error when scopes are valid but not permitted for grant_type' do
|
110
|
+
allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string('public'))
|
111
|
+
allow(Doorkeeper.configuration).to receive(:scopes_by_grant_type).and_return(password: 'another')
|
112
|
+
subject.validate
|
113
|
+
expect(subject.error).to eq(:invalid_scope)
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'creates the token with scopes if scopes are valid and permitted for grant_type' do
|
117
|
+
allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string('public'))
|
118
|
+
allow(Doorkeeper.configuration).to receive(:scopes_by_grant_type).and_return(password: [:public])
|
119
|
+
expect do
|
120
|
+
subject.authorize
|
121
|
+
end.to change { Doorkeeper::AccessToken.count }.by(1)
|
122
|
+
|
123
|
+
expect(Doorkeeper::AccessToken.last.scopes).to include('public')
|
124
|
+
end
|
104
125
|
end
|
105
126
|
end
|
106
127
|
|
@@ -91,6 +91,18 @@ module Doorkeeper::OAuth
|
|
91
91
|
subject.scope = 'invalid'
|
92
92
|
expect(subject).not_to be_authorizable
|
93
93
|
end
|
94
|
+
|
95
|
+
it 'accepts scopes which are permitted for grant_type' do
|
96
|
+
allow(server).to receive(:scopes_by_grant_type).and_return(authorization_code: [:public])
|
97
|
+
subject.scope = 'public'
|
98
|
+
expect(subject).to be_authorizable
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'rejects scopes which are not permitted for grant_type' do
|
102
|
+
allow(server).to receive(:scopes_by_grant_type).and_return(authorization_code: [:profile])
|
103
|
+
subject.scope = 'public'
|
104
|
+
expect(subject).not_to be_authorizable
|
105
|
+
end
|
94
106
|
end
|
95
107
|
|
96
108
|
context 'client application restricts valid scopes' do
|
@@ -114,6 +126,18 @@ module Doorkeeper::OAuth
|
|
114
126
|
subject.scope = 'profile'
|
115
127
|
expect(subject).to_not be_authorizable
|
116
128
|
end
|
129
|
+
|
130
|
+
it 'accepts scopes which are permitted for grant_type' do
|
131
|
+
allow(server).to receive(:scopes_by_grant_type).and_return(authorization_code: [:public])
|
132
|
+
subject.scope = 'public'
|
133
|
+
expect(subject).to be_authorizable
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'rejects scopes which are not permitted for grant_type' do
|
137
|
+
allow(server).to receive(:scopes_by_grant_type).and_return(authorization_code: [:profile])
|
138
|
+
subject.scope = 'public'
|
139
|
+
expect(subject).not_to be_authorizable
|
140
|
+
end
|
117
141
|
end
|
118
142
|
|
119
143
|
it 'uses default scopes when none is required' do
|
@@ -17,13 +17,13 @@ module Doorkeeper::OAuth
|
|
17
17
|
describe '.body' do
|
18
18
|
let(:access_token) do
|
19
19
|
double :access_token,
|
20
|
-
|
21
|
-
expires_in:
|
22
|
-
expires_in_seconds:
|
23
|
-
scopes_string:
|
24
|
-
|
25
|
-
token_type:
|
26
|
-
created_at:
|
20
|
+
plaintext_token: "some-token",
|
21
|
+
expires_in: "3600",
|
22
|
+
expires_in_seconds: "300",
|
23
|
+
scopes_string: "two scopes",
|
24
|
+
plaintext_refresh_token: "some-refresh-token",
|
25
|
+
token_type: "bearer",
|
26
|
+
created_at: 0
|
27
27
|
end
|
28
28
|
|
29
29
|
subject { TokenResponse.new(access_token).body }
|
@@ -58,12 +58,12 @@ module Doorkeeper::OAuth
|
|
58
58
|
describe '.body filters out empty values' do
|
59
59
|
let(:access_token) do
|
60
60
|
double :access_token,
|
61
|
-
|
62
|
-
expires_in_seconds:
|
63
|
-
scopes_string:
|
64
|
-
|
65
|
-
token_type:
|
66
|
-
created_at:
|
61
|
+
plaintext_token: 'some-token',
|
62
|
+
expires_in_seconds: '',
|
63
|
+
scopes_string: '',
|
64
|
+
plaintext_refresh_token: '',
|
65
|
+
token_type: 'bearer',
|
66
|
+
created_at: 0
|
67
67
|
end
|
68
68
|
|
69
69
|
subject { TokenResponse.new(access_token).body }
|
@@ -113,6 +113,20 @@ module Doorkeeper
|
|
113
113
|
end
|
114
114
|
end
|
115
115
|
|
116
|
+
context 'token hashing is enabled' do
|
117
|
+
include_context 'with token hashing enabled'
|
118
|
+
|
119
|
+
let(:hashed_token) { hashed_or_plain_token_func.call('token') }
|
120
|
+
let(:token) { ->(_r) { 'token' } }
|
121
|
+
|
122
|
+
it 'searches with the hashed token' do
|
123
|
+
expect(
|
124
|
+
AccessToken
|
125
|
+
).to receive(:find_by).with(token: hashed_token).and_return(token)
|
126
|
+
Token.authenticate double, token
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
116
130
|
context 'refresh tokens are enabled' do
|
117
131
|
before do
|
118
132
|
Doorkeeper.configure do
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Doorkeeper::AccessGrant do
|
4
|
+
let(:clazz) { Doorkeeper::AccessGrant }
|
4
5
|
subject { FactoryBot.build(:access_grant) }
|
5
6
|
|
6
7
|
it { expect(subject).to be_valid }
|
@@ -11,6 +12,66 @@ describe Doorkeeper::AccessGrant do
|
|
11
12
|
let(:factory_name) { :access_grant }
|
12
13
|
end
|
13
14
|
|
15
|
+
context 'with hashing enabled' do
|
16
|
+
let(:grant) { FactoryBot.create :access_grant }
|
17
|
+
include_context 'with token hashing enabled'
|
18
|
+
|
19
|
+
it 'holds a volatile plaintext token when created' do
|
20
|
+
expect(grant.plaintext_token).to be_a(String)
|
21
|
+
expect(grant.token)
|
22
|
+
.to eq(hashed_or_plain_token_func.call(grant.plaintext_token))
|
23
|
+
|
24
|
+
# Finder method only finds the hashed token
|
25
|
+
loaded = clazz.find_by(token: grant.token)
|
26
|
+
expect(loaded).to eq(grant)
|
27
|
+
expect(loaded.plaintext_token).to be_nil
|
28
|
+
expect(loaded.token).to eq(grant.token)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'does not find_by plain text tokens' do
|
32
|
+
expect(clazz.find_by(token: grant.plaintext_token)).to be_nil
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'with having a plain text token' do
|
36
|
+
let(:plain_text_token) { 'plain text token' }
|
37
|
+
|
38
|
+
before do
|
39
|
+
# Assume we have a plain text token from before activating the option
|
40
|
+
grant.update_column(:token, plain_text_token)
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'without fallback lookup' do
|
44
|
+
it 'does not provide lookups with either through by_token' do
|
45
|
+
expect(clazz.by_token(plain_text_token)).to eq(nil)
|
46
|
+
expect(clazz.by_token(grant.token)).to eq(nil)
|
47
|
+
|
48
|
+
# And it does not touch the token
|
49
|
+
grant.reload
|
50
|
+
expect(grant.token).to eq(plain_text_token)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'with fallback lookup' do
|
55
|
+
include_context 'with token hashing and fallback lookup enabled'
|
56
|
+
|
57
|
+
it 'upgrades a plain token when falling back to it' do
|
58
|
+
# Side-effect: This will automatically upgrade the token
|
59
|
+
expect(clazz).to receive(:upgrade_fallback_value).and_call_original
|
60
|
+
expect(clazz.by_token(plain_text_token)).to eq(grant)
|
61
|
+
|
62
|
+
# Will find subsequently by hashing the token
|
63
|
+
expect(clazz.by_token(plain_text_token)).to eq(grant)
|
64
|
+
|
65
|
+
# And it modifies the token value
|
66
|
+
grant.reload
|
67
|
+
expect(grant.token).not_to eq(plain_text_token)
|
68
|
+
expect(clazz.find_by(token: plain_text_token)).to eq(nil)
|
69
|
+
expect(clazz.find_by(token: grant.token)).not_to be_nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
14
75
|
describe 'validations' do
|
15
76
|
it 'is invalid without resource_owner_id' do
|
16
77
|
subject.resource_owner_id = nil
|
@@ -2,6 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
module Doorkeeper
|
4
4
|
describe AccessToken do
|
5
|
+
let(:clazz) { Doorkeeper::AccessToken }
|
5
6
|
subject { FactoryBot.build(:access_token) }
|
6
7
|
|
7
8
|
it { expect(subject).to be_valid }
|
@@ -24,6 +25,67 @@ module Doorkeeper
|
|
24
25
|
expect(token.token).to be_a(String)
|
25
26
|
end
|
26
27
|
|
28
|
+
context 'with hashing enabled' do
|
29
|
+
let(:token) { FactoryBot.create :access_token }
|
30
|
+
include_context 'with token hashing enabled'
|
31
|
+
|
32
|
+
it 'holds a volatile plaintext token when created' do
|
33
|
+
expect(token.plaintext_token).to be_a(String)
|
34
|
+
expect(token.token)
|
35
|
+
.to eq(hashed_or_plain_token_func.call(token.plaintext_token))
|
36
|
+
|
37
|
+
# Finder method only finds the hashed token
|
38
|
+
loaded = clazz.find_by(token: token.token)
|
39
|
+
expect(loaded).to eq(token)
|
40
|
+
expect(loaded.plaintext_token).to be_nil
|
41
|
+
expect(loaded.token).to eq(token.token)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'does not find_by plain text tokens' do
|
45
|
+
expect(clazz.find_by(token: token.plaintext_token)).to be_nil
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'with having a plain text token' do
|
49
|
+
let(:plain_text_token) { 'plain text token' }
|
50
|
+
let(:access_token) { FactoryBot.create :access_token }
|
51
|
+
|
52
|
+
before do
|
53
|
+
# Assume we have a plain text token from before activating the option
|
54
|
+
access_token.update_column(:token, plain_text_token)
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'without fallback lookup' do
|
58
|
+
it 'does not provide lookups with either through by_token' do
|
59
|
+
expect(clazz.by_token(plain_text_token)).to eq(nil)
|
60
|
+
expect(clazz.by_token(access_token.token)).to eq(nil)
|
61
|
+
|
62
|
+
# And it does not touch the token
|
63
|
+
access_token.reload
|
64
|
+
expect(access_token.token).to eq(plain_text_token)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'with fallback lookup' do
|
69
|
+
include_context 'with token hashing and fallback lookup enabled'
|
70
|
+
|
71
|
+
it 'upgrades a plain token when falling back to it' do
|
72
|
+
# Side-effect: This will automatically upgrade the token
|
73
|
+
expect(clazz).to receive(:upgrade_fallback_value).and_call_original
|
74
|
+
expect(clazz.by_token(plain_text_token)).to eq(access_token)
|
75
|
+
|
76
|
+
# Will find subsequently by hashing the token
|
77
|
+
expect(clazz.by_token(plain_text_token)).to eq(access_token)
|
78
|
+
|
79
|
+
# And it modifies the token value
|
80
|
+
access_token.reload
|
81
|
+
expect(access_token.token).not_to eq(plain_text_token)
|
82
|
+
expect(clazz.find_by(token: plain_text_token)).to eq(nil)
|
83
|
+
expect(clazz.find_by(token: access_token.token)).not_to be_nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
27
89
|
it 'generates a token using a custom object' do
|
28
90
|
eigenclass = class << CustomGeneratorArgs; self; end
|
29
91
|
eigenclass.class_eval do
|
@@ -196,6 +258,67 @@ module Doorkeeper
|
|
196
258
|
token2.save(validate: false)
|
197
259
|
end.to raise_error(uniqueness_error)
|
198
260
|
end
|
261
|
+
|
262
|
+
context 'with hashing enabled' do
|
263
|
+
include_context 'with token hashing enabled'
|
264
|
+
let(:token) { FactoryBot.create :access_token, use_refresh_token: true }
|
265
|
+
|
266
|
+
it 'holds a volatile refresh token when created' do
|
267
|
+
expect(token.plaintext_refresh_token).to be_a(String)
|
268
|
+
expect(token.refresh_token)
|
269
|
+
.to eq(hashed_or_plain_token_func.call(token.plaintext_refresh_token))
|
270
|
+
|
271
|
+
# Finder method only finds the hashed token
|
272
|
+
loaded = clazz.find_by(refresh_token: token.refresh_token)
|
273
|
+
expect(loaded).to eq(token)
|
274
|
+
expect(loaded.plaintext_refresh_token).to be_nil
|
275
|
+
expect(loaded.refresh_token).to eq(token.refresh_token)
|
276
|
+
end
|
277
|
+
|
278
|
+
it 'does not find_by plain text refresh tokens' do
|
279
|
+
expect(clazz.find_by(refresh_token: token.plaintext_refresh_token)).to be_nil
|
280
|
+
end
|
281
|
+
|
282
|
+
describe 'with having a plain text token' do
|
283
|
+
let(:plain_refresh_token) { 'plain refresh token' }
|
284
|
+
let(:access_token) { FactoryBot.create :access_token }
|
285
|
+
|
286
|
+
before do
|
287
|
+
# Assume we have a plain text token from before activating the option
|
288
|
+
access_token.update_column(:refresh_token, plain_refresh_token)
|
289
|
+
end
|
290
|
+
|
291
|
+
context 'without fallback lookup' do
|
292
|
+
it 'does not provide lookups with either through by_token' do
|
293
|
+
expect(clazz.by_refresh_token(plain_refresh_token)).to eq(nil)
|
294
|
+
expect(clazz.by_refresh_token(access_token.refresh_token)).to eq(nil)
|
295
|
+
|
296
|
+
# And it does not touch the token
|
297
|
+
access_token.reload
|
298
|
+
expect(access_token.refresh_token).to eq(plain_refresh_token)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
context 'with fallback lookup' do
|
303
|
+
include_context 'with token hashing and fallback lookup enabled'
|
304
|
+
|
305
|
+
it 'upgrades a plain token when falling back to it' do
|
306
|
+
# Side-effect: This will automatically upgrade the token
|
307
|
+
expect(clazz).to receive(:upgrade_fallback_value).and_call_original
|
308
|
+
expect(clazz.by_refresh_token(plain_refresh_token)).to eq(access_token)
|
309
|
+
|
310
|
+
# Will find subsequently by hashing the token
|
311
|
+
expect(clazz.by_refresh_token(plain_refresh_token)).to eq(access_token)
|
312
|
+
|
313
|
+
# And it modifies the token value
|
314
|
+
access_token.reload
|
315
|
+
expect(access_token.refresh_token).not_to eq(plain_refresh_token)
|
316
|
+
expect(clazz.find_by(refresh_token: plain_refresh_token)).to eq(nil)
|
317
|
+
expect(clazz.find_by(refresh_token: access_token.refresh_token)).not_to be_nil
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
199
322
|
end
|
200
323
|
|
201
324
|
describe 'validations' do
|
@@ -1,374 +1,306 @@
|
|
1
|
-
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
|
-
|
3
|
+
module Doorkeeper
|
4
|
+
describe Application do
|
5
|
+
let(:clazz) { Doorkeeper::Application }
|
6
|
+
let(:require_owner) { Doorkeeper.configuration.instance_variable_set('@confirm_application_owner', true) }
|
7
|
+
let(:unset_require_owner) { Doorkeeper.configuration.instance_variable_set('@confirm_application_owner', false) }
|
8
|
+
let(:new_application) { FactoryBot.build(:application) }
|
4
9
|
|
5
|
-
|
6
|
-
|
7
|
-
let(:unset_require_owner) { Doorkeeper.configuration.instance_variable_set("@confirm_application_owner", false) }
|
8
|
-
let(:new_application) { FactoryBot.build(:application) }
|
10
|
+
let(:uid) { SecureRandom.hex(8) }
|
11
|
+
let(:secret) { SecureRandom.hex(8) }
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
it "is invalid without determining confidentiality" do
|
19
|
-
new_application.confidential = nil
|
20
|
-
expect(new_application).not_to be_valid
|
21
|
-
end
|
22
|
-
|
23
|
-
it "generates uid on create" do
|
24
|
-
expect(new_application.uid).to be_nil
|
25
|
-
new_application.save
|
26
|
-
expect(new_application.uid).not_to be_nil
|
27
|
-
end
|
28
|
-
|
29
|
-
it "generates uid on create if an empty string" do
|
30
|
-
new_application.uid = ""
|
31
|
-
new_application.save
|
32
|
-
expect(new_application.uid).not_to be_blank
|
33
|
-
end
|
34
|
-
|
35
|
-
it "generates uid on create unless one is set" do
|
36
|
-
new_application.uid = uid
|
37
|
-
new_application.save
|
38
|
-
expect(new_application.uid).to eq(uid)
|
39
|
-
end
|
40
|
-
|
41
|
-
it "is invalid without uid" do
|
42
|
-
new_application.save
|
43
|
-
new_application.uid = nil
|
44
|
-
expect(new_application).not_to be_valid
|
45
|
-
end
|
46
|
-
|
47
|
-
it "checks uniqueness of uid" do
|
48
|
-
app1 = FactoryBot.create(:application)
|
49
|
-
app2 = FactoryBot.create(:application)
|
50
|
-
app2.uid = app1.uid
|
51
|
-
expect(app2).not_to be_valid
|
52
|
-
end
|
53
|
-
|
54
|
-
it "expects database to throw an error when uids are the same" do
|
55
|
-
app1 = FactoryBot.create(:application)
|
56
|
-
app2 = FactoryBot.create(:application)
|
57
|
-
app2.uid = app1.uid
|
58
|
-
expect { app2.save!(validate: false) }.to raise_error(uniqueness_error)
|
59
|
-
end
|
13
|
+
context 'application_owner is enabled' do
|
14
|
+
before do
|
15
|
+
Doorkeeper.configure do
|
16
|
+
orm DOORKEEPER_ORM
|
17
|
+
enable_application_owner
|
18
|
+
end
|
19
|
+
end
|
60
20
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
end
|
21
|
+
context 'application owner is not required' do
|
22
|
+
before(:each) do
|
23
|
+
unset_require_owner
|
24
|
+
end
|
66
25
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
end
|
26
|
+
it 'is valid given valid attributes' do
|
27
|
+
expect(new_application).to be_valid
|
28
|
+
end
|
29
|
+
end
|
72
30
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
31
|
+
context 'application owner is required' do
|
32
|
+
before(:each) do
|
33
|
+
require_owner
|
34
|
+
@owner = FactoryBot.build_stubbed(:doorkeeper_testing_user)
|
35
|
+
end
|
78
36
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
expect(new_application).not_to be_valid
|
83
|
-
end
|
37
|
+
it 'is invalid without an owner' do
|
38
|
+
expect(new_application).not_to be_valid
|
39
|
+
end
|
84
40
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
enable_application_owner
|
41
|
+
it 'is valid with an owner' do
|
42
|
+
new_application.owner = @owner
|
43
|
+
expect(new_application).to be_valid
|
44
|
+
end
|
90
45
|
end
|
91
46
|
end
|
92
47
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
48
|
+
it 'is invalid without a name' do
|
49
|
+
new_application.name = nil
|
50
|
+
expect(new_application).not_to be_valid
|
51
|
+
end
|
97
52
|
|
98
|
-
|
99
|
-
|
100
|
-
|
53
|
+
it 'is invalid without determining confidentiality' do
|
54
|
+
new_application.confidential = nil
|
55
|
+
expect(new_application).not_to be_valid
|
101
56
|
end
|
102
57
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
58
|
+
it 'generates uid on create' do
|
59
|
+
expect(new_application.uid).to be_nil
|
60
|
+
new_application.save
|
61
|
+
expect(new_application.uid).not_to be_nil
|
62
|
+
end
|
108
63
|
|
109
|
-
|
110
|
-
|
111
|
-
|
64
|
+
it 'generates uid on create if an empty string' do
|
65
|
+
new_application.uid = ''
|
66
|
+
new_application.save
|
67
|
+
expect(new_application.uid).not_to be_blank
|
68
|
+
end
|
112
69
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
70
|
+
it 'generates uid on create unless one is set' do
|
71
|
+
new_application.uid = uid
|
72
|
+
new_application.save
|
73
|
+
expect(new_application.uid).to eq(uid)
|
117
74
|
end
|
118
|
-
end
|
119
75
|
|
120
|
-
|
121
|
-
it "is invalid without redirect_uri" do
|
76
|
+
it 'is invalid without uid' do
|
122
77
|
new_application.save
|
123
|
-
new_application.
|
78
|
+
new_application.uid = nil
|
124
79
|
expect(new_application).not_to be_valid
|
125
80
|
end
|
126
|
-
end
|
127
81
|
|
128
|
-
|
129
|
-
before(:each) do
|
82
|
+
it 'is invalid without redirect_uri' do
|
130
83
|
new_application.save
|
84
|
+
new_application.redirect_uri = nil
|
85
|
+
expect(new_application).not_to be_valid
|
131
86
|
end
|
132
87
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
application: new_application,
|
139
|
-
resource_owner_id: resource_owner.id,
|
140
|
-
)
|
141
|
-
|
142
|
-
expect { new_application.destroy }.to change { Doorkeeper::AccessGrant.count }.by(-1)
|
88
|
+
it 'checks uniqueness of uid' do
|
89
|
+
app1 = FactoryBot.create(:application)
|
90
|
+
app2 = FactoryBot.create(:application)
|
91
|
+
app2.uid = app1.uid
|
92
|
+
expect(app2).not_to be_valid
|
143
93
|
end
|
144
94
|
|
145
|
-
it
|
146
|
-
FactoryBot.create(:
|
147
|
-
FactoryBot.create(:
|
148
|
-
|
149
|
-
|
150
|
-
end.to change { Doorkeeper::AccessToken.count }.by(-2)
|
95
|
+
it 'expects database to throw an error when uids are the same' do
|
96
|
+
app1 = FactoryBot.create(:application)
|
97
|
+
app2 = FactoryBot.create(:application)
|
98
|
+
app2.uid = app1.uid
|
99
|
+
expect { app2.save!(validate: false) }.to raise_error(uniqueness_error)
|
151
100
|
end
|
152
|
-
end
|
153
101
|
|
154
|
-
|
155
|
-
|
102
|
+
it 'generate secret on create' do
|
103
|
+
expect(new_application.secret).to be_nil
|
104
|
+
new_application.save
|
105
|
+
expect(new_application.secret).not_to be_nil
|
106
|
+
end
|
156
107
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
end
|
108
|
+
it 'generate secret on create if is blank string' do
|
109
|
+
new_application.secret = ''
|
110
|
+
new_application.save
|
111
|
+
expect(new_application.secret).not_to be_blank
|
162
112
|
end
|
163
113
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
end
|
114
|
+
it 'generate secret on create unless one is set' do
|
115
|
+
new_application.secret = secret
|
116
|
+
new_application.save
|
117
|
+
expect(new_application.secret).to eq(secret)
|
169
118
|
end
|
170
|
-
end
|
171
119
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
expect(new_application.redirect_uri).to eq("http://localhost/callback1\nhttp://localhost/callback2")
|
177
|
-
end
|
120
|
+
it 'is invalid without secret' do
|
121
|
+
new_application.save
|
122
|
+
new_application.secret = nil
|
123
|
+
expect(new_application).not_to be_valid
|
178
124
|
end
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
125
|
+
|
126
|
+
context 'with hashing enabled' do
|
127
|
+
include_context 'with application hashing enabled'
|
128
|
+
let(:app) { FactoryBot.create :application }
|
129
|
+
|
130
|
+
it 'holds a volatile plaintext and BCrypt secret' do
|
131
|
+
expect(app.plaintext_secret).to be_a(String)
|
132
|
+
expect(app.secret).not_to eq(app.plaintext_secret)
|
133
|
+
expect { BCrypt::Password.create(app.secret) }.not_to raise_error
|
183
134
|
end
|
184
|
-
end
|
185
|
-
end
|
186
135
|
|
187
|
-
|
188
|
-
|
189
|
-
|
136
|
+
it 'does not fallback to plain lookup by default' do
|
137
|
+
lookup = clazz.by_uid_and_secret(app.uid, app.secret)
|
138
|
+
expect(lookup).to eq(nil)
|
190
139
|
|
191
|
-
|
192
|
-
|
193
|
-
|
140
|
+
lookup = clazz.by_uid_and_secret(app.uid, app.plaintext_secret)
|
141
|
+
expect(lookup).to eq(app)
|
142
|
+
end
|
194
143
|
|
195
|
-
|
196
|
-
|
197
|
-
:access_token,
|
198
|
-
resource_owner_id: other_resource_owner.id,
|
199
|
-
)
|
200
|
-
token = FactoryBot.create(
|
201
|
-
:access_token,
|
202
|
-
resource_owner_id: resource_owner.id,
|
203
|
-
)
|
204
|
-
expect(described_class.authorized_for(resource_owner)).to eq([token.application])
|
205
|
-
end
|
144
|
+
context 'with fallback enabled' do
|
145
|
+
include_context 'with token hashing and fallback lookup enabled'
|
206
146
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
resource_owner_id: resource_owner.id,
|
211
|
-
revoked_at: 2.days.ago,
|
212
|
-
)
|
213
|
-
expect(described_class.authorized_for(resource_owner)).to be_empty
|
214
|
-
end
|
147
|
+
it 'provides plain and hashed lookup' do
|
148
|
+
lookup = clazz.by_uid_and_secret(app.uid, app.secret)
|
149
|
+
expect(lookup).to eq(app)
|
215
150
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
)
|
221
|
-
token2 = FactoryBot.create(
|
222
|
-
:access_token,
|
223
|
-
resource_owner_id: resource_owner.id,
|
224
|
-
)
|
225
|
-
expect(described_class.authorized_for(resource_owner))
|
226
|
-
.to eq([token1.application, token2.application])
|
227
|
-
end
|
151
|
+
lookup = clazz.by_uid_and_secret(app.uid, app.plaintext_secret)
|
152
|
+
expect(lookup).to eq(app)
|
153
|
+
end
|
154
|
+
end
|
228
155
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
resource_owner_id: resource_owner.id,
|
234
|
-
application: application,
|
235
|
-
)
|
236
|
-
FactoryBot.create(
|
237
|
-
:access_token,
|
238
|
-
resource_owner_id: resource_owner.id,
|
239
|
-
application: application,
|
240
|
-
)
|
241
|
-
expect(described_class.authorized_for(resource_owner)).to eq([application])
|
156
|
+
it 'does not provide access to secret after loading' do
|
157
|
+
lookup = clazz.by_uid_and_secret(app.uid, app.plaintext_secret)
|
158
|
+
expect(lookup.plaintext_secret).to be_nil
|
159
|
+
end
|
242
160
|
end
|
243
|
-
end
|
244
161
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
expect(Doorkeeper::AccessToken)
|
250
|
-
.to receive(:revoke_all_for).with(application_id, resource_owner)
|
251
|
-
expect(Doorkeeper::AccessGrant)
|
252
|
-
.to receive(:revoke_all_for).with(application_id, resource_owner)
|
162
|
+
describe 'destroy related models on cascade' do
|
163
|
+
before(:each) do
|
164
|
+
new_application.save
|
165
|
+
end
|
253
166
|
|
254
|
-
|
167
|
+
it 'should destroy its access grants' do
|
168
|
+
FactoryBot.create(:access_grant, application: new_application)
|
169
|
+
expect { new_application.destroy }.to change { Doorkeeper::AccessGrant.count }.by(-1)
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'should destroy its access tokens' do
|
173
|
+
FactoryBot.create(:access_token, application: new_application)
|
174
|
+
FactoryBot.create(:access_token, application: new_application, revoked_at: Time.now.utc)
|
175
|
+
expect do
|
176
|
+
new_application.destroy
|
177
|
+
end.to change { Doorkeeper::AccessToken.count }.by(-2)
|
178
|
+
end
|
255
179
|
end
|
256
|
-
end
|
257
180
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
181
|
+
describe :ordered_by do
|
182
|
+
let(:applications) { FactoryBot.create_list(:application, 5) }
|
183
|
+
|
184
|
+
context 'when a direction is not specified' do
|
185
|
+
it 'calls order with a default order of asc' do
|
186
|
+
names = applications.map(&:name).sort
|
187
|
+
expect(Application.ordered_by(:name).map(&:name)).to eq(names)
|
188
|
+
end
|
264
189
|
end
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
expect(
|
190
|
+
|
191
|
+
context 'when a direction is specified' do
|
192
|
+
it 'calls order with specified direction' do
|
193
|
+
names = applications.map(&:name).sort.reverse
|
194
|
+
expect(Application.ordered_by(:name, :desc).map(&:name)).to eq(names)
|
270
195
|
end
|
271
196
|
end
|
272
197
|
end
|
273
198
|
|
274
|
-
|
275
|
-
context "when
|
276
|
-
it "should
|
277
|
-
|
278
|
-
|
279
|
-
expect(authenticated).to eq(app)
|
199
|
+
describe "#redirect_uri=" do
|
200
|
+
context "when array of valid redirect_uris" do
|
201
|
+
it "should join by newline" do
|
202
|
+
new_application.redirect_uri = ['http://localhost/callback1', 'http://localhost/callback2']
|
203
|
+
expect(new_application.redirect_uri).to eq("http://localhost/callback1\nhttp://localhost/callback2")
|
280
204
|
end
|
281
205
|
end
|
282
|
-
context "when
|
283
|
-
it "should
|
284
|
-
|
285
|
-
|
286
|
-
expect(authenticated).to eq(nil)
|
206
|
+
context "when string of valid redirect_uris" do
|
207
|
+
it "should store as-is" do
|
208
|
+
new_application.redirect_uri = "http://localhost/callback1\nhttp://localhost/callback2"
|
209
|
+
expect(new_application.redirect_uri).to eq("http://localhost/callback1\nhttp://localhost/callback2")
|
287
210
|
end
|
288
211
|
end
|
289
212
|
end
|
290
|
-
end
|
291
|
-
|
292
|
-
describe "#confidential?" do
|
293
|
-
subject { FactoryBot.create(:application, confidential: confidential).confidential? }
|
294
|
-
|
295
|
-
context "when application is private/confidential" do
|
296
|
-
let(:confidential) { true }
|
297
|
-
it { expect(subject).to eq(true) }
|
298
|
-
end
|
299
213
|
|
300
|
-
|
301
|
-
let(:
|
302
|
-
it { expect(subject).to eq(false) }
|
303
|
-
end
|
304
|
-
end
|
214
|
+
describe :authorized_for do
|
215
|
+
let(:resource_owner) { double(:resource_owner, id: 10) }
|
305
216
|
|
306
|
-
|
307
|
-
|
217
|
+
it 'is empty if the application is not authorized for anyone' do
|
218
|
+
expect(Application.authorized_for(resource_owner)).to be_empty
|
219
|
+
end
|
308
220
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
expect(app.to_json(include_root_in_json: true)).to match(/application.+?:\{/)
|
314
|
-
ActiveRecord::Base.include_root_in_json = false
|
221
|
+
it 'returns only application for a specific resource owner' do
|
222
|
+
FactoryBot.create(:access_token, resource_owner_id: resource_owner.id + 1)
|
223
|
+
token = FactoryBot.create(:access_token, resource_owner_id: resource_owner.id)
|
224
|
+
expect(Application.authorized_for(resource_owner)).to eq([token.application])
|
315
225
|
end
|
316
|
-
end
|
317
226
|
|
318
|
-
|
319
|
-
|
320
|
-
expect(
|
321
|
-
"id" => app.id,
|
322
|
-
"name" => app.name,
|
323
|
-
"created_at" => anything,
|
324
|
-
)
|
227
|
+
it 'excludes revoked tokens' do
|
228
|
+
FactoryBot.create(:access_token, resource_owner_id: resource_owner.id, revoked_at: 2.days.ago)
|
229
|
+
expect(Application.authorized_for(resource_owner)).to be_empty
|
325
230
|
end
|
326
231
|
|
327
|
-
it
|
328
|
-
|
232
|
+
it 'returns all applications that have been authorized' do
|
233
|
+
token1 = FactoryBot.create(:access_token, resource_owner_id: resource_owner.id)
|
234
|
+
token2 = FactoryBot.create(:access_token, resource_owner_id: resource_owner.id)
|
235
|
+
expect(Application.authorized_for(resource_owner)).to eq([token1.application, token2.application])
|
236
|
+
end
|
329
237
|
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
)
|
238
|
+
it 'returns only one application even if it has been authorized twice' do
|
239
|
+
application = FactoryBot.create(:application)
|
240
|
+
FactoryBot.create(:access_token, resource_owner_id: resource_owner.id, application: application)
|
241
|
+
FactoryBot.create(:access_token, resource_owner_id: resource_owner.id, application: application)
|
242
|
+
expect(Application.authorized_for(resource_owner)).to eq([application])
|
336
243
|
end
|
244
|
+
end
|
245
|
+
|
246
|
+
describe :revoke_tokens_and_grants_for do
|
247
|
+
it 'revokes all access tokens and access grants' do
|
248
|
+
application_id = 42
|
249
|
+
resource_owner = double
|
250
|
+
expect(Doorkeeper::AccessToken)
|
251
|
+
.to receive(:revoke_all_for).with(application_id, resource_owner)
|
252
|
+
expect(Doorkeeper::AccessGrant)
|
253
|
+
.to receive(:revoke_all_for).with(application_id, resource_owner)
|
337
254
|
|
338
|
-
|
339
|
-
expect(app.as_json(except: :id)).not_to include("id")
|
340
|
-
expect(app.as_json(only: %i[name created_at secret]))
|
341
|
-
.to match(
|
342
|
-
"name" => app.name,
|
343
|
-
"created_at" => anything,
|
344
|
-
)
|
255
|
+
Application.revoke_tokens_and_grants_for(application_id, resource_owner)
|
345
256
|
end
|
346
257
|
end
|
347
258
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
259
|
+
describe :by_uid_and_secret do
|
260
|
+
context "when application is private/confidential" do
|
261
|
+
it "finds the application via uid/secret" do
|
262
|
+
app = FactoryBot.create :application
|
263
|
+
authenticated = Application.by_uid_and_secret(app.uid, app.secret)
|
264
|
+
expect(authenticated).to eq(app)
|
265
|
+
end
|
266
|
+
context "when secret is wrong" do
|
267
|
+
it "should not find the application" do
|
268
|
+
app = FactoryBot.create :application
|
269
|
+
authenticated = Application.by_uid_and_secret(app.uid, 'bad')
|
270
|
+
expect(authenticated).to eq(nil)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
352
274
|
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
275
|
+
context "when application is public/non-confidential" do
|
276
|
+
context "when secret is blank" do
|
277
|
+
it "should find the application" do
|
278
|
+
app = FactoryBot.create :application, confidential: false
|
279
|
+
authenticated = Application.by_uid_and_secret(app.uid, nil)
|
280
|
+
expect(authenticated).to eq(app)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
context "when secret is wrong" do
|
284
|
+
it "should not find the application" do
|
285
|
+
app = FactoryBot.create :application, confidential: false
|
286
|
+
authenticated = Application.by_uid_and_secret(app.uid, 'bad')
|
287
|
+
expect(authenticated).to eq(nil)
|
288
|
+
end
|
357
289
|
end
|
358
290
|
end
|
291
|
+
end
|
292
|
+
|
293
|
+
describe :confidential? do
|
294
|
+
subject { FactoryBot.create(:application, confidential: confidential).confidential? }
|
359
295
|
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
"secret" => "123123123",
|
364
|
-
"redirect_uri" => app.redirect_uri,
|
365
|
-
"uid" => app.uid,
|
366
|
-
)
|
296
|
+
context 'when application is private/confidential' do
|
297
|
+
let(:confidential) { true }
|
298
|
+
it { expect(subject).to eq(true) }
|
367
299
|
end
|
368
300
|
|
369
|
-
|
370
|
-
|
371
|
-
|
301
|
+
context 'when application is public/non-confidential' do
|
302
|
+
let(:confidential) { false }
|
303
|
+
it { expect(subject).to eq(false) }
|
372
304
|
end
|
373
305
|
end
|
374
306
|
end
|