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.

Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +7 -3
  3. data/Dangerfile +5 -2
  4. data/Gemfile +3 -1
  5. data/NEWS.md +20 -13
  6. data/README.md +1 -1
  7. data/app/controllers/doorkeeper/applications_controller.rb +3 -3
  8. data/app/controllers/doorkeeper/authorized_applications_controller.rb +1 -1
  9. data/app/controllers/doorkeeper/tokens_controller.rb +6 -6
  10. data/app/views/doorkeeper/applications/show.html.erb +1 -1
  11. data/app/views/layouts/doorkeeper/admin.html.erb +5 -3
  12. data/bin/console +15 -0
  13. data/gemfiles/rails_4_2.gemfile +1 -0
  14. data/gemfiles/rails_5_0.gemfile +1 -0
  15. data/gemfiles/rails_5_1.gemfile +1 -0
  16. data/gemfiles/rails_5_2.gemfile +2 -1
  17. data/gemfiles/rails_master.gemfile +1 -0
  18. data/lib/doorkeeper.rb +1 -0
  19. data/lib/doorkeeper/config.rb +73 -6
  20. data/lib/doorkeeper/helpers/controller.rb +3 -2
  21. data/lib/doorkeeper/models/access_grant_mixin.rb +8 -1
  22. data/lib/doorkeeper/models/access_token_mixin.rb +40 -9
  23. data/lib/doorkeeper/models/application_mixin.rb +52 -1
  24. data/lib/doorkeeper/models/concerns/hashable.rb +137 -0
  25. data/lib/doorkeeper/models/concerns/scopes.rb +1 -1
  26. data/lib/doorkeeper/oauth/authorization/code.rb +1 -1
  27. data/lib/doorkeeper/oauth/authorization/token.rb +1 -1
  28. data/lib/doorkeeper/oauth/authorization_code_request.rb +1 -1
  29. data/lib/doorkeeper/oauth/client.rb +1 -1
  30. data/lib/doorkeeper/oauth/client_credentials/validation.rb +4 -3
  31. data/lib/doorkeeper/oauth/code_response.rb +2 -2
  32. data/lib/doorkeeper/oauth/helpers/scope_checker.rb +23 -8
  33. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +32 -0
  34. data/lib/doorkeeper/oauth/password_access_token_request.rb +7 -2
  35. data/lib/doorkeeper/oauth/pre_authorization.rb +8 -3
  36. data/lib/doorkeeper/oauth/refresh_token_request.rb +4 -1
  37. data/lib/doorkeeper/oauth/token_response.rb +2 -2
  38. data/lib/doorkeeper/orm/active_record/access_grant.rb +22 -2
  39. data/lib/doorkeeper/orm/active_record/application.rb +12 -53
  40. data/lib/doorkeeper/version.rb +3 -3
  41. data/lib/generators/doorkeeper/templates/initializer.rb +41 -1
  42. data/spec/controllers/application_metal_controller_spec.rb +18 -4
  43. data/spec/controllers/tokens_controller_spec.rb +7 -11
  44. data/spec/dummy/app/controllers/application_controller.rb +1 -1
  45. data/spec/factories.rb +3 -3
  46. data/spec/lib/config_spec.rb +84 -0
  47. data/spec/lib/models/hashable_spec.rb +183 -0
  48. data/spec/lib/oauth/base_request_spec.rb +7 -7
  49. data/spec/lib/oauth/client_credentials/validation_spec.rb +3 -0
  50. data/spec/lib/oauth/helpers/scope_checker_spec.rb +52 -17
  51. data/spec/lib/oauth/helpers/uri_checker_spec.rb +20 -2
  52. data/spec/lib/oauth/password_access_token_request_spec.rb +32 -11
  53. data/spec/lib/oauth/pre_authorization_spec.rb +24 -0
  54. data/spec/lib/oauth/token_response_spec.rb +13 -13
  55. data/spec/lib/oauth/token_spec.rb +14 -0
  56. data/spec/models/doorkeeper/access_grant_spec.rb +61 -0
  57. data/spec/models/doorkeeper/access_token_spec.rb +123 -0
  58. data/spec/models/doorkeeper/application_spec.rb +227 -295
  59. data/spec/requests/flows/authorization_code_spec.rb +40 -0
  60. data/spec/requests/flows/password_spec.rb +4 -2
  61. data/spec/requests/flows/revoke_token_spec.rb +14 -30
  62. data/spec/spec_helper.rb +2 -1
  63. data/spec/support/ruby_2_6_rails_4_2_patch.rb +14 -0
  64. data/spec/support/shared/hashing_shared_context.rb +29 -0
  65. 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
- it 'validates the current scope' do
92
- allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string('another'))
93
- subject.validate
94
- expect(subject.error).to eq(:invalid_scope)
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
- it 'creates the token with scopes' do
98
- allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string('public'))
99
- expect do
100
- subject.authorize
101
- end.to change { Doorkeeper::AccessToken.count }.by(1)
102
-
103
- expect(Doorkeeper::AccessToken.last.scopes).to include('public')
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
- token: 'some-token',
21
- expires_in: '3600',
22
- expires_in_seconds: '300',
23
- scopes_string: 'two scopes',
24
- refresh_token: 'some-refresh-token',
25
- token_type: 'bearer',
26
- created_at: 0
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
- token: 'some-token',
62
- expires_in_seconds: '',
63
- scopes_string: '',
64
- refresh_token: '',
65
- token_type: 'bearer',
66
- created_at: 0
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
- # frozen_string_literal: true
1
+ require 'spec_helper'
2
2
 
3
- require "spec_helper"
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
- describe Doorkeeper::Application do
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) }
10
+ let(:uid) { SecureRandom.hex(8) }
11
+ let(:secret) { SecureRandom.hex(8) }
9
12
 
10
- let(:uid) { SecureRandom.hex(8) }
11
- let(:secret) { SecureRandom.hex(8) }
12
-
13
- it "is invalid without a name" do
14
- new_application.name = nil
15
- expect(new_application).not_to be_valid
16
- end
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
- it "generate secret on create" do
62
- expect(new_application.secret).to be_nil
63
- new_application.save
64
- expect(new_application.secret).not_to be_nil
65
- end
21
+ context 'application owner is not required' do
22
+ before(:each) do
23
+ unset_require_owner
24
+ end
66
25
 
67
- it "generate secret on create if is blank string" do
68
- new_application.secret = ""
69
- new_application.save
70
- expect(new_application.secret).not_to be_blank
71
- end
26
+ it 'is valid given valid attributes' do
27
+ expect(new_application).to be_valid
28
+ end
29
+ end
72
30
 
73
- it "generate secret on create unless one is set" do
74
- new_application.secret = secret
75
- new_application.save
76
- expect(new_application.secret).to eq(secret)
77
- end
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
- it "is invalid without secret" do
80
- new_application.save
81
- new_application.secret = nil
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
- context "application_owner is enabled" do
86
- before do
87
- Doorkeeper.configure do
88
- orm DOORKEEPER_ORM
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
- context "application owner is not required" do
94
- before(:each) do
95
- unset_require_owner
96
- end
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
- it "is valid given valid attributes" do
99
- expect(new_application).to be_valid
100
- end
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
- context "application owner is required" do
104
- before do
105
- require_owner
106
- @owner = FactoryBot.build_stubbed(:doorkeeper_testing_user)
107
- end
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
- it "is invalid without an owner" do
110
- expect(new_application).not_to be_valid
111
- end
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
- it "is valid with an owner" do
114
- new_application.owner = @owner
115
- expect(new_application).to be_valid
116
- end
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
- context "redirect URI" do
121
- it "is invalid without redirect_uri" do
76
+ it 'is invalid without uid' do
122
77
  new_application.save
123
- new_application.redirect_uri = nil
78
+ new_application.uid = nil
124
79
  expect(new_application).not_to be_valid
125
80
  end
126
- end
127
81
 
128
- describe "destroy related models on cascade" do
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
- let(:resource_owner) { FactoryBot.create(:doorkeeper_testing_user) }
134
-
135
- it "should destroy its access grants" do
136
- FactoryBot.create(
137
- :access_grant,
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 "should destroy its access tokens" do
146
- FactoryBot.create(:access_token, application: new_application)
147
- FactoryBot.create(:access_token, application: new_application, revoked_at: Time.now.utc)
148
- expect do
149
- new_application.destroy
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
- describe "#ordered_by" do
155
- let(:applications) { FactoryBot.create_list(:application, 5) }
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
- context "when a direction is not specified" do
158
- it "calls order with a default order of asc" do
159
- names = applications.map(&:name).sort
160
- expect(described_class.ordered_by(:name).map(&:name)).to eq(names)
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
- context "when a direction is specified" do
165
- it "calls order with specified direction" do
166
- names = applications.map(&:name).sort.reverse
167
- expect(described_class.ordered_by(:name, :desc).map(&:name)).to eq(names)
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
- describe "#redirect_uri=" do
173
- context "when array of valid redirect_uris" do
174
- it "should join by newline" do
175
- new_application.redirect_uri = ["http://localhost/callback1", "http://localhost/callback2"]
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
- context "when string of valid redirect_uris" do
180
- it "should store as-is" do
181
- new_application.redirect_uri = "http://localhost/callback1\nhttp://localhost/callback2"
182
- expect(new_application.redirect_uri).to eq("http://localhost/callback1\nhttp://localhost/callback2")
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
- describe "#authorized_for" do
188
- let(:resource_owner) { FactoryBot.create(:doorkeeper_testing_user) }
189
- let(:other_resource_owner) { FactoryBot.create(:doorkeeper_testing_user) }
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
- it "is empty if the application is not authorized for anyone" do
192
- expect(described_class.authorized_for(resource_owner)).to be_empty
193
- end
140
+ lookup = clazz.by_uid_and_secret(app.uid, app.plaintext_secret)
141
+ expect(lookup).to eq(app)
142
+ end
194
143
 
195
- it "returns only application for a specific resource owner" do
196
- FactoryBot.create(
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
- it "excludes revoked tokens" do
208
- FactoryBot.create(
209
- :access_token,
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
- it "returns all applications that have been authorized" do
217
- token1 = FactoryBot.create(
218
- :access_token,
219
- resource_owner_id: resource_owner.id,
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
- it "returns only one application even if it has been authorized twice" do
230
- application = FactoryBot.create(:application)
231
- FactoryBot.create(
232
- :access_token,
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
- describe "#revoke_tokens_and_grants_for" do
246
- it "revokes all access tokens and access grants" do
247
- application_id = 42
248
- resource_owner = double
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
- described_class.revoke_tokens_and_grants_for(application_id, resource_owner)
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
- describe "#by_uid_and_secret" do
259
- context "when application is private/confidential" do
260
- it "finds the application via uid/secret" do
261
- app = FactoryBot.create :application
262
- authenticated = described_class.by_uid_and_secret(app.uid, app.secret)
263
- expect(authenticated).to eq(app)
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
- context "when secret is wrong" do
266
- it "should not find the application" do
267
- app = FactoryBot.create :application
268
- authenticated = described_class.by_uid_and_secret(app.uid, "bad")
269
- expect(authenticated).to eq(nil)
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
- context "when application is public/non-confidential" do
275
- context "when secret is blank" do
276
- it "should find the application" do
277
- app = FactoryBot.create :application, confidential: false
278
- authenticated = described_class.by_uid_and_secret(app.uid, nil)
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 secret is wrong" do
283
- it "should not find the application" do
284
- app = FactoryBot.create :application, confidential: false
285
- authenticated = described_class.by_uid_and_secret(app.uid, "bad")
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
- context "when application is public/non-confidential" do
301
- let(:confidential) { false }
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
- describe "#as_json" do
307
- let(:app) { FactoryBot.create :application, secret: "123123123" }
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
- # AR specific feature
310
- if DOORKEEPER_ORM == :active_record
311
- it "correctly works with #to_json" do
312
- ActiveRecord::Base.include_root_in_json = true
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
- context "when called without authorized resource owner" do
319
- it "includes minimal set of attributes" do
320
- expect(app.as_json).to match(
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 "includes application UID if it's public" do
328
- app = FactoryBot.create :application, secret: "123123123", confidential: false
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
- expect(app.as_json).to match(
331
- "id" => app.id,
332
- "name" => app.name,
333
- "created_at" => anything,
334
- "uid" => app.uid,
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
- it "respects custom options" do
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
- context "when called with authorized resource owner" do
349
- let(:owner) { FactoryBot.create(:doorkeeper_testing_user) }
350
- let(:other_owner) { FactoryBot.create(:doorkeeper_testing_user) }
351
- let(:app) { FactoryBot.create(:application, secret: "123123123", owner: owner) }
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
- before do
354
- Doorkeeper.configure do
355
- orm DOORKEEPER_ORM
356
- enable_application_owner confirmation: false
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
- it "includes all the attributes" do
361
- expect(app.as_json(current_resource_owner: owner))
362
- .to include(
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
- it "doesn't include unsafe attributes if current owner isn't the same as owner" do
370
- expect(app.as_json(current_resource_owner: other_owner))
371
- .not_to include("redirect_uri")
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