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.

Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +11 -2
  3. data/Appraisals +29 -3
  4. data/Gemfile +13 -5
  5. data/NEWS.md +52 -15
  6. data/README.md +68 -487
  7. data/app/controllers/doorkeeper/token_info_controller.rb +1 -1
  8. data/app/controllers/doorkeeper/tokens_controller.rb +1 -1
  9. data/doorkeeper.gemspec +3 -2
  10. data/gemfiles/rails_4_2.gemfile +8 -5
  11. data/gemfiles/rails_5_0.gemfile +9 -6
  12. data/gemfiles/rails_5_1.gemfile +9 -6
  13. data/gemfiles/rails_5_2.gemfile +9 -6
  14. data/gemfiles/rails_6_0.gemfile +16 -0
  15. data/gemfiles/rails_master.gemfile +8 -10
  16. data/lib/doorkeeper.rb +7 -1
  17. data/lib/doorkeeper/config.rb +110 -24
  18. data/lib/doorkeeper/models/access_grant_mixin.rb +15 -7
  19. data/lib/doorkeeper/models/access_token_mixin.rb +29 -16
  20. data/lib/doorkeeper/models/application_mixin.rb +18 -28
  21. data/lib/doorkeeper/models/concerns/expirable.rb +3 -2
  22. data/lib/doorkeeper/models/concerns/reusable.rb +19 -0
  23. data/lib/doorkeeper/models/concerns/scopes.rb +4 -0
  24. data/lib/doorkeeper/models/concerns/secret_storable.rb +106 -0
  25. data/lib/doorkeeper/oauth/authorization/token.rb +3 -1
  26. data/lib/doorkeeper/oauth/error_response.rb +5 -1
  27. data/lib/doorkeeper/oauth/helpers/unique_token.rb +12 -1
  28. data/lib/doorkeeper/oauth/invalid_token_response.rb +4 -0
  29. data/lib/doorkeeper/oauth/token_introspection.rb +72 -6
  30. data/lib/doorkeeper/orm/active_record/access_grant.rb +9 -8
  31. data/lib/doorkeeper/orm/active_record/application.rb +10 -6
  32. data/lib/doorkeeper/secret_storing/base.rb +63 -0
  33. data/lib/doorkeeper/secret_storing/bcrypt.rb +59 -0
  34. data/lib/doorkeeper/secret_storing/plain.rb +33 -0
  35. data/lib/doorkeeper/secret_storing/sha256_hash.rb +25 -0
  36. data/lib/doorkeeper/version.rb +1 -1
  37. data/lib/generators/doorkeeper/templates/initializer.rb +62 -20
  38. data/spec/controllers/authorizations_controller_spec.rb +3 -3
  39. data/spec/controllers/token_info_controller_spec.rb +1 -1
  40. data/spec/controllers/tokens_controller_spec.rb +78 -30
  41. data/spec/dummy/config/application.rb +12 -1
  42. data/spec/lib/config_spec.rb +119 -35
  43. data/spec/lib/models/expirable_spec.rb +12 -0
  44. data/spec/lib/models/reusable_spec.rb +40 -0
  45. data/spec/lib/models/scopes_spec.rb +13 -1
  46. data/spec/lib/models/secret_storable_spec.rb +113 -0
  47. data/spec/lib/oauth/authorization_code_request_spec.rb +18 -1
  48. data/spec/lib/oauth/client_credentials/creator_spec.rb +51 -7
  49. data/spec/lib/oauth/error_response_spec.rb +7 -1
  50. data/spec/lib/oauth/password_access_token_request_spec.rb +11 -1
  51. data/spec/lib/oauth/token_request_spec.rb +16 -1
  52. data/spec/lib/secret_storing/base_spec.rb +60 -0
  53. data/spec/lib/secret_storing/bcrypt_spec.rb +49 -0
  54. data/spec/lib/secret_storing/plain_spec.rb +44 -0
  55. data/spec/lib/secret_storing/sha256_hash_spec.rb +48 -0
  56. data/spec/models/doorkeeper/application_spec.rb +23 -4
  57. data/spec/requests/flows/authorization_code_spec.rb +3 -3
  58. data/spec/requests/flows/client_credentials_spec.rb +2 -2
  59. data/spec/requests/flows/implicit_grant_spec.rb +1 -1
  60. data/spec/requests/flows/password_spec.rb +3 -3
  61. data/spec/routing/custom_controller_routes_spec.rb +4 -0
  62. data/spec/support/shared/hashing_shared_context.rb +12 -5
  63. metadata +51 -21
  64. data/lib/doorkeeper/models/concerns/hashable.rb +0 -137
  65. data/spec/lib/models/hashable_spec.rb +0 -183
@@ -15,15 +15,59 @@ class Doorkeeper::OAuth::ClientCredentialsRequest
15
15
  end.to change { Doorkeeper::AccessToken.count }.by(1)
16
16
  end
17
17
 
18
- context "when reuse_access_token is true" do
19
- it "returns the existing valid token" do
20
- allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true)
21
- existing_token = subject.call(client, scopes)
18
+ context 'when reuse_access_token is true' do
19
+ context 'when expiration is disabled' do
20
+ it 'returns the existing valid token' do
21
+ allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true)
22
+ existing_token = subject.call(client, scopes)
22
23
 
23
- result = subject.call(client, scopes)
24
+ result = subject.call(client, scopes)
25
+
26
+ expect(Doorkeeper::AccessToken.count).to eq(1)
27
+ expect(result).to eq(existing_token)
28
+ end
29
+ end
30
+
31
+ context 'when existing token has not crossed token_reuse_limit' do
32
+ it 'returns the existing valid token' do
33
+ allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true)
34
+ allow(Doorkeeper.configuration).to receive(:token_reuse_limit).and_return(50)
35
+ existing_token = subject.call(client, scopes, expires_in: 1000)
36
+
37
+ allow_any_instance_of(Doorkeeper::AccessToken).to receive(:expires_in_seconds).and_return(600)
38
+ result = subject.call(client, scopes, expires_in: 1000)
39
+
40
+ expect(Doorkeeper::AccessToken.count).to eq(1)
41
+ expect(result).to eq(existing_token)
42
+ end
43
+ end
44
+
45
+ context 'when existing token has crossed token_reuse_limit' do
46
+ it "returns a new token" do
47
+ allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true)
48
+ allow(Doorkeeper.configuration).to receive(:token_reuse_limit).and_return(50)
49
+ existing_token = subject.call(client, scopes, expires_in: 1000)
50
+
51
+ allow_any_instance_of(Doorkeeper::AccessToken).to receive(:expires_in_seconds).and_return(400)
52
+ result = subject.call(client, scopes, expires_in: 1000)
53
+
54
+ expect(Doorkeeper::AccessToken.count).to eq(2)
55
+ expect(result).not_to eq(existing_token)
56
+ end
57
+ end
58
+
59
+ context 'when existing token has been expired' do
60
+ it "returns a new token" do
61
+ allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true)
62
+ allow(Doorkeeper.configuration).to receive(:token_reuse_limit).and_return(50)
63
+ existing_token = subject.call(client, scopes, expires_in: 1000)
64
+
65
+ allow_any_instance_of(Doorkeeper::AccessToken).to receive(:expired?).and_return(true)
66
+ result = subject.call(client, scopes, expires_in: 1000)
24
67
 
25
- expect(Doorkeeper::AccessToken.count).to eq(1)
26
- expect(result).to eq(existing_token)
68
+ expect(Doorkeeper::AccessToken.count).to eq(2)
69
+ expect(result).not_to eq(existing_token)
70
+ end
27
71
  end
28
72
  end
29
73
 
@@ -3,7 +3,13 @@ require 'spec_helper'
3
3
  module Doorkeeper::OAuth
4
4
  describe ErrorResponse do
5
5
  describe '#status' do
6
- it 'should have a status of unauthorized' do
6
+ it 'should have a status of bad_request' do
7
+ expect(subject.status).to eq(:bad_request)
8
+ end
9
+
10
+ it 'should have a status of unauthorized for an invalid_client error' do
11
+ subject = described_class.new(name: :invalid_client)
12
+
7
13
  expect(subject.status).to eq(:unauthorized)
8
14
  end
9
15
  end
@@ -64,7 +64,7 @@ module Doorkeeper::OAuth
64
64
  end.to change { Doorkeeper::AccessToken.count }.by(1)
65
65
  end
66
66
 
67
- it 'skips token creation if there is already one' do
67
+ it 'skips token creation if there is already one reusable' do
68
68
  allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true)
69
69
  FactoryBot.create(:access_token, application_id: client.id, resource_owner_id: owner.id)
70
70
 
@@ -73,6 +73,16 @@ module Doorkeeper::OAuth
73
73
  end.not_to(change { Doorkeeper::AccessToken.count })
74
74
  end
75
75
 
76
+ it 'creates token when there is already one but non reusable' do
77
+ allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true)
78
+ FactoryBot.create(:access_token, application_id: client.id, resource_owner_id: owner.id)
79
+ allow_any_instance_of(Doorkeeper::AccessToken).to receive(:reusable?).and_return(false)
80
+
81
+ expect do
82
+ subject.authorize
83
+ end.to change { Doorkeeper::AccessToken.count }.by(1)
84
+ end
85
+
76
86
  it "calls configured request callback methods" do
77
87
  expect(Doorkeeper.configuration.before_successful_strategy_response)
78
88
  .to receive(:call).with(subject).once
@@ -80,7 +80,7 @@ module Doorkeeper::OAuth
80
80
  end.to change { Doorkeeper::AccessToken.count }.by(1)
81
81
  end
82
82
 
83
- it 'skips token creation if there is a matching one' do
83
+ it 'skips token creation if there is a matching one reusable' do
84
84
  allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true)
85
85
  allow(application.scopes).to receive(:has_scopes?).and_return(true)
86
86
  allow(application.scopes).to receive(:all?).and_return(true)
@@ -90,6 +90,21 @@ module Doorkeeper::OAuth
90
90
 
91
91
  expect { subject.authorize }.not_to(change { Doorkeeper::AccessToken.count })
92
92
  end
93
+
94
+ it 'creates new token if there is a matching one but non reusable' do
95
+ allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true)
96
+ allow(application.scopes).to receive(:has_scopes?).and_return(true)
97
+ allow(application.scopes).to receive(:all?).and_return(true)
98
+
99
+ FactoryBot.create(:access_token, application_id: pre_auth.client.id,
100
+ resource_owner_id: owner.id, scopes: 'public')
101
+
102
+ allow_any_instance_of(Doorkeeper::AccessToken).to receive(:reusable?).and_return(false)
103
+
104
+ expect do
105
+ subject.authorize
106
+ end.to change { Doorkeeper::AccessToken.count }.by(1)
107
+ end
93
108
  end
94
109
  end
95
110
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ::Doorkeeper::SecretStoring::Base do
6
+ let(:instance) { double('instance', token: 'foo') }
7
+ subject { described_class }
8
+
9
+ describe '#transform_secret' do
10
+ it 'raises' do
11
+ expect { subject.transform_secret('foo') }.to raise_error(NotImplementedError)
12
+ end
13
+ end
14
+
15
+ describe '#store_secret' do
16
+ it 'sends to response of #transform_secret to the instance' do
17
+ expect(described_class)
18
+ .to receive(:transform_secret).with('bar')
19
+ .and_return 'bar+transform'
20
+
21
+ expect(instance).to receive(:token=).with 'bar+transform'
22
+ result = subject.store_secret instance, :token, 'bar'
23
+ expect(result).to eq 'bar+transform'
24
+ end
25
+ end
26
+
27
+ describe '#restore_secret' do
28
+ it 'raises' do
29
+ expect { subject.restore_secret(subject, :token) }.to raise_error(NotImplementedError)
30
+ end
31
+ end
32
+
33
+ describe '#allows_restoring_secrets?' do
34
+ it 'does not allow it' do
35
+ expect(subject.allows_restoring_secrets?).to eq false
36
+ end
37
+ end
38
+
39
+ describe 'validate_for' do
40
+ it 'allows for valid model' do
41
+ expect(subject.validate_for(:application)).to eq true
42
+ expect(subject.validate_for(:token)).to eq true
43
+ end
44
+
45
+ it 'raises for invalid model' do
46
+ expect { subject.validate_for(:wat) }.to raise_error(ArgumentError, /can not be used for wat/)
47
+ end
48
+ end
49
+
50
+ describe 'secret_matches?' do
51
+ before do
52
+ allow(subject).to receive(:transform_secret) { |input| "transformed: #{input}" }
53
+ end
54
+
55
+ it 'compares input with #transform_secret' do
56
+ expect(subject.secret_matches?('input', 'input')).to eq false
57
+ expect(subject.secret_matches?('a', 'transformed: a')).to eq true
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'bcrypt'
5
+
6
+ describe ::Doorkeeper::SecretStoring::BCrypt do
7
+ subject { described_class }
8
+ let(:instance) { double('instance', token: 'foo') }
9
+
10
+ describe '#transform_secret' do
11
+ it 'creates a bcrypt password' do
12
+ expect(subject.transform_secret('foo')).to be_a BCrypt::Password
13
+ end
14
+ end
15
+
16
+ describe '#restore_secret' do
17
+ it 'raises' do
18
+ expect { subject.restore_secret(instance, :token) }.to raise_error(NotImplementedError)
19
+ end
20
+ end
21
+
22
+ describe '#allows_restoring_secrets?' do
23
+ it 'does not allow it' do
24
+ expect(subject.allows_restoring_secrets?).to eq false
25
+ end
26
+ end
27
+
28
+ describe 'validate_for' do
29
+ it 'allows for valid model' do
30
+ expect(subject.validate_for(:application)).to eq true
31
+ end
32
+
33
+ it 'raises for invalid model' do
34
+ expect { subject.validate_for(:wat) }
35
+ .to raise_error(ArgumentError, /can only be used for storing application secrets/)
36
+ expect { subject.validate_for(:token) }
37
+ .to raise_error(ArgumentError, /can only be used for storing application secrets/)
38
+ end
39
+ end
40
+
41
+ describe 'secret_matches?' do
42
+ it 'compares input with #transform_secret' do
43
+ expect(subject.secret_matches?('input', 'input')).to eq false
44
+
45
+ password = BCrypt::Password.create 'foobar'
46
+ expect(subject.secret_matches?('foobar', password.to_s)).to eq true
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ::Doorkeeper::SecretStoring::Plain do
6
+ subject { described_class }
7
+ let(:instance) { double('instance', token: 'foo') }
8
+
9
+ describe '#transform_secret' do
10
+ it 'raises' do
11
+ expect(subject.transform_secret('foo')).to eq 'foo'
12
+ end
13
+ end
14
+
15
+ describe '#restore_secret' do
16
+ it 'raises' do
17
+ expect(subject.restore_secret(instance, :token)).to eq 'foo'
18
+ end
19
+ end
20
+
21
+ describe '#allows_restoring_secrets?' do
22
+ it 'does allow it' do
23
+ expect(subject.allows_restoring_secrets?).to eq true
24
+ end
25
+ end
26
+
27
+ describe 'validate_for' do
28
+ it 'allows for valid model' do
29
+ expect(subject.validate_for(:application)).to eq true
30
+ expect(subject.validate_for(:token)).to eq true
31
+ end
32
+
33
+ it 'raises for invalid model' do
34
+ expect { subject.validate_for(:wat) }.to raise_error(ArgumentError, /can not be used for wat/)
35
+ end
36
+ end
37
+
38
+ describe 'secret_matches?' do
39
+ it 'compares input with #transform_secret' do
40
+ expect(subject.secret_matches?('input', 'input')).to eq true
41
+ expect(subject.secret_matches?('a', 'b')).to eq false
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ::Doorkeeper::SecretStoring::Sha256Hash do
6
+ subject { described_class }
7
+ let(:instance) { double('instance') }
8
+
9
+ let(:hash_function) do
10
+ ->(input) { ::Digest::SHA256.hexdigest(input) }
11
+ end
12
+
13
+ describe '#transform_secret' do
14
+ it 'raises' do
15
+ expect(subject.transform_secret('foo')).to eq hash_function.call('foo')
16
+ end
17
+ end
18
+
19
+ describe '#restore_secret' do
20
+ it 'raises' do
21
+ expect { subject.restore_secret(instance, :token) }.to raise_error(NotImplementedError)
22
+ end
23
+ end
24
+
25
+ describe '#allows_restoring_secrets?' do
26
+ it 'does not allow it' do
27
+ expect(subject.allows_restoring_secrets?).to eq false
28
+ end
29
+ end
30
+
31
+ describe 'validate_for' do
32
+ it 'allows for valid model' do
33
+ expect(subject.validate_for(:application)).to eq true
34
+ expect(subject.validate_for(:token)).to eq true
35
+ end
36
+
37
+ it 'raises for invalid model' do
38
+ expect { subject.validate_for(:wat) }.to raise_error(ArgumentError, /can not be used for wat/)
39
+ end
40
+ end
41
+
42
+ describe 'secret_matches?' do
43
+ it 'compares input with #transform_secret' do
44
+ expect(subject.secret_matches?('input', 'input')).to eq false
45
+ expect(subject.secret_matches?('a', hash_function.call('a'))).to eq true
46
+ end
47
+ end
48
+ end
@@ -1,4 +1,5 @@
1
1
  require 'spec_helper'
2
+ require 'bcrypt'
2
3
 
3
4
  module Doorkeeper
4
5
  describe Application do
@@ -126,11 +127,29 @@ module Doorkeeper
126
127
  context 'with hashing enabled' do
127
128
  include_context 'with application hashing enabled'
128
129
  let(:app) { FactoryBot.create :application }
130
+ let(:default_strategy) { Doorkeeper::SecretStoring::Sha256Hash }
129
131
 
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
132
+ it 'uses SHA256 to avoid additional dependencies' do
133
+ # Ensure token was generated
134
+ app.validate
135
+ expect(app.secret).to eq(default_strategy.transform_secret(app.plaintext_secret))
136
+ end
137
+
138
+ context 'when bcrypt strategy is configured' do
139
+ # In this text context, we have bcrypt loaded so `bcrypt_present?`
140
+ # will always be true
141
+ before do
142
+ Doorkeeper.configure do
143
+ hash_application_secrets using: 'Doorkeeper::SecretStoring::BCrypt'
144
+ end
145
+ end
146
+
147
+ it 'holds a volatile plaintext and BCrypt secret' do
148
+ expect(app.secret_strategy).to eq Doorkeeper::SecretStoring::BCrypt
149
+ expect(app.plaintext_secret).to be_a(String)
150
+ expect(app.secret).not_to eq(app.plaintext_secret)
151
+ expect { ::BCrypt::Password.create(app.secret) }.not_to raise_error
152
+ end
134
153
  end
135
154
 
136
155
  it 'does not fallback to plain lookup by default' do
@@ -23,7 +23,7 @@ feature 'Authorization Code Flow' do
23
23
 
24
24
  context 'with grant hashing enabled' do
25
25
  background do
26
- config_is_set(:hash_token_secrets) { true }
26
+ config_is_set(:token_secret_strategy, ::Doorkeeper::SecretStoring::Sha256Hash)
27
27
  end
28
28
 
29
29
  scenario 'Authorization Code Flow with hashing' do
@@ -37,7 +37,7 @@ feature 'Authorization Code Flow' do
37
37
  code = current_params['code']
38
38
  expect(code).not_to be_nil
39
39
 
40
- hashed_code = Doorkeeper::AccessGrant.hashed_or_plain_token code
40
+ hashed_code = Doorkeeper::AccessGrant.secret_strategy.transform_secret code
41
41
  expect(hashed_code).to eq Doorkeeper::AccessGrant.first.token
42
42
 
43
43
  expect(code).not_to eq(hashed_code)
@@ -330,7 +330,7 @@ feature 'Authorization Code Flow' do
330
330
 
331
331
  context 'when application scopes are present and no scope is passed' do
332
332
  background do
333
- @client.update_attributes(scopes: 'public write read')
333
+ @client.update(scopes: 'public write read')
334
334
  end
335
335
 
336
336
  scenario 'access grant has no scope' do
@@ -58,7 +58,7 @@ describe 'Client Credentials Request' do
58
58
  should_have_json 'error_description', translated_error_message(:invalid_scope)
59
59
  should_not_have_json 'access_token'
60
60
 
61
- expect(response.status).to eq(401)
61
+ expect(response.status).to eq(400)
62
62
  end
63
63
  end
64
64
  end
@@ -66,7 +66,7 @@ describe 'Client Credentials Request' do
66
66
 
67
67
  context 'when application scopes contain some of the default scopes and no scope is passed' do
68
68
  before do
69
- client.update_attributes(scopes: 'read write public')
69
+ client.update(scopes: 'read write public')
70
70
  end
71
71
 
72
72
  it 'issues new token with one default scope that are present in application scopes' do
@@ -20,7 +20,7 @@ feature 'Implicit Grant Flow (feature spec)' do
20
20
 
21
21
  context 'when application scopes are present and no scope is passed' do
22
22
  background do
23
- @client.update_attributes(scopes: 'public write read')
23
+ @client.update(scopes: 'public write read')
24
24
  end
25
25
 
26
26
  scenario 'access token has no scopes' do
@@ -149,7 +149,7 @@ describe 'Resource Owner Password Credentials Flow' do
149
149
  context 'when application scopes are present and differs from configured default scopes and no scope is passed' do
150
150
  before do
151
151
  default_scopes_exist :public
152
- @client.update_attributes(scopes: 'abc')
152
+ @client.update(scopes: 'abc')
153
153
  end
154
154
 
155
155
  it 'issues new token without any scope' do
@@ -168,7 +168,7 @@ describe 'Resource Owner Password Credentials Flow' do
168
168
 
169
169
  context 'when application scopes contain some of the default scopes and no scope is passed' do
170
170
  before do
171
- @client.update_attributes(scopes: 'read write public')
171
+ @client.update(scopes: 'read write public')
172
172
  end
173
173
 
174
174
  it 'issues new token with one default scope that are present in application scopes' do
@@ -217,7 +217,7 @@ describe 'Resource Owner Password Credentials Flow' do
217
217
  should_have_json 'error_description', translated_error_message(:invalid_scope)
218
218
  should_not_have_json 'access_token'
219
219
 
220
- expect(response.status).to eq(401)
220
+ expect(response.status).to eq(400)
221
221
  end
222
222
  end
223
223