doorkeeper 5.0.2 → 5.1.0.rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.travis.yml +17 -4
- data/Appraisals +29 -3
- data/Dangerfile +5 -2
- data/Gemfile +14 -4
- data/NEWS.md +64 -19
- data/README.md +68 -487
- data/app/controllers/doorkeeper/token_info_controller.rb +1 -1
- data/app/controllers/doorkeeper/tokens_controller.rb +7 -7
- 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/doorkeeper.gemspec +3 -2
- data/gemfiles/rails_4_2.gemfile +8 -4
- data/gemfiles/rails_5_0.gemfile +9 -5
- data/gemfiles/rails_5_1.gemfile +9 -5
- data/gemfiles/rails_5_2.gemfile +9 -5
- data/gemfiles/rails_6_0.gemfile +16 -0
- data/gemfiles/rails_master.gemfile +8 -9
- data/lib/doorkeeper/config.rb +164 -11
- data/lib/doorkeeper/helpers/controller.rb +3 -2
- data/lib/doorkeeper/models/access_grant_mixin.rb +16 -1
- data/lib/doorkeeper/models/access_token_mixin.rb +54 -10
- data/lib/doorkeeper/models/application_mixin.rb +42 -1
- data/lib/doorkeeper/models/concerns/expirable.rb +3 -2
- data/lib/doorkeeper/models/concerns/reusable.rb +19 -0
- data/lib/doorkeeper/models/concerns/scopes.rb +5 -1
- data/lib/doorkeeper/models/concerns/secret_storable.rb +106 -0
- data/lib/doorkeeper/oauth/authorization/code.rb +1 -1
- data/lib/doorkeeper/oauth/authorization/token.rb +4 -2
- 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/error_response.rb +5 -1
- data/lib/doorkeeper/oauth/helpers/scope_checker.rb +23 -8
- data/lib/doorkeeper/oauth/helpers/unique_token.rb +12 -1
- data/lib/doorkeeper/oauth/helpers/uri_checker.rb +32 -0
- data/lib/doorkeeper/oauth/invalid_token_response.rb +4 -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_introspection.rb +72 -6
- data/lib/doorkeeper/oauth/token_response.rb +2 -2
- data/lib/doorkeeper/orm/active_record/access_grant.rb +23 -2
- data/lib/doorkeeper/orm/active_record/application.rb +20 -2
- data/lib/doorkeeper/secret_storing/base.rb +63 -0
- data/lib/doorkeeper/secret_storing/bcrypt.rb +59 -0
- data/lib/doorkeeper/secret_storing/plain.rb +33 -0
- data/lib/doorkeeper/secret_storing/sha256_hash.rb +25 -0
- data/lib/doorkeeper/version.rb +3 -3
- data/lib/doorkeeper.rb +7 -0
- data/lib/generators/doorkeeper/templates/initializer.rb +90 -8
- data/spec/controllers/application_metal_controller_spec.rb +18 -4
- data/spec/controllers/authorizations_controller_spec.rb +3 -3
- data/spec/controllers/token_info_controller_spec.rb +1 -1
- data/spec/controllers/tokens_controller_spec.rb +85 -41
- data/spec/dummy/app/controllers/application_controller.rb +1 -1
- data/spec/dummy/config/application.rb +12 -1
- data/spec/factories.rb +3 -3
- data/spec/lib/config_spec.rb +168 -0
- data/spec/lib/models/expirable_spec.rb +12 -0
- data/spec/lib/models/reusable_spec.rb +40 -0
- data/spec/lib/models/scopes_spec.rb +13 -1
- data/spec/lib/models/secret_storable_spec.rb +113 -0
- data/spec/lib/oauth/authorization_code_request_spec.rb +18 -1
- data/spec/lib/oauth/base_request_spec.rb +7 -7
- data/spec/lib/oauth/client_credentials/creator_spec.rb +51 -7
- data/spec/lib/oauth/client_credentials/validation_spec.rb +3 -0
- data/spec/lib/oauth/error_response_spec.rb +7 -1
- 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 +43 -12
- data/spec/lib/oauth/pre_authorization_spec.rb +24 -0
- data/spec/lib/oauth/token_request_spec.rb +16 -1
- data/spec/lib/oauth/token_response_spec.rb +13 -13
- data/spec/lib/oauth/token_spec.rb +14 -0
- data/spec/lib/secret_storing/base_spec.rb +60 -0
- data/spec/lib/secret_storing/bcrypt_spec.rb +49 -0
- data/spec/lib/secret_storing/plain_spec.rb +44 -0
- data/spec/lib/secret_storing/sha256_hash_spec.rb +48 -0
- data/spec/models/doorkeeper/access_grant_spec.rb +61 -0
- data/spec/models/doorkeeper/access_token_spec.rb +123 -0
- data/spec/models/doorkeeper/application_spec.rb +56 -0
- data/spec/requests/flows/authorization_code_spec.rb +41 -1
- data/spec/requests/flows/client_credentials_spec.rb +2 -2
- data/spec/requests/flows/implicit_grant_spec.rb +1 -1
- data/spec/requests/flows/password_spec.rb +7 -5
- data/spec/requests/flows/revoke_token_spec.rb +14 -30
- data/spec/routing/custom_controller_routes_spec.rb +4 -0
- 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 +36 -0
- metadata +59 -22
|
@@ -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,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,7 +1,9 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
|
+
require 'bcrypt'
|
|
2
3
|
|
|
3
4
|
module Doorkeeper
|
|
4
5
|
describe Application do
|
|
6
|
+
let(:clazz) { Doorkeeper::Application }
|
|
5
7
|
let(:require_owner) { Doorkeeper.configuration.instance_variable_set('@confirm_application_owner', true) }
|
|
6
8
|
let(:unset_require_owner) { Doorkeeper.configuration.instance_variable_set('@confirm_application_owner', false) }
|
|
7
9
|
let(:new_application) { FactoryBot.build(:application) }
|
|
@@ -122,6 +124,60 @@ module Doorkeeper
|
|
|
122
124
|
expect(new_application).not_to be_valid
|
|
123
125
|
end
|
|
124
126
|
|
|
127
|
+
context 'with hashing enabled' do
|
|
128
|
+
include_context 'with application hashing enabled'
|
|
129
|
+
let(:app) { FactoryBot.create :application }
|
|
130
|
+
let(:default_strategy) { Doorkeeper::SecretStoring::Sha256Hash }
|
|
131
|
+
|
|
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
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'does not fallback to plain lookup by default' do
|
|
156
|
+
lookup = clazz.by_uid_and_secret(app.uid, app.secret)
|
|
157
|
+
expect(lookup).to eq(nil)
|
|
158
|
+
|
|
159
|
+
lookup = clazz.by_uid_and_secret(app.uid, app.plaintext_secret)
|
|
160
|
+
expect(lookup).to eq(app)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
context 'with fallback enabled' do
|
|
164
|
+
include_context 'with token hashing and fallback lookup enabled'
|
|
165
|
+
|
|
166
|
+
it 'provides plain and hashed lookup' do
|
|
167
|
+
lookup = clazz.by_uid_and_secret(app.uid, app.secret)
|
|
168
|
+
expect(lookup).to eq(app)
|
|
169
|
+
|
|
170
|
+
lookup = clazz.by_uid_and_secret(app.uid, app.plaintext_secret)
|
|
171
|
+
expect(lookup).to eq(app)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'does not provide access to secret after loading' do
|
|
176
|
+
lookup = clazz.by_uid_and_secret(app.uid, app.plaintext_secret)
|
|
177
|
+
expect(lookup.plaintext_secret).to be_nil
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
125
181
|
describe 'destroy related models on cascade' do
|
|
126
182
|
before(:each) do
|
|
127
183
|
new_application.save
|
|
@@ -21,6 +21,33 @@ feature 'Authorization Code Flow' do
|
|
|
21
21
|
url_should_not_have_param('error')
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
context 'with grant hashing enabled' do
|
|
25
|
+
background do
|
|
26
|
+
config_is_set(:token_secret_strategy, ::Doorkeeper::SecretStoring::Sha256Hash)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
scenario 'Authorization Code Flow with hashing' do
|
|
30
|
+
@client.redirect_uri = Doorkeeper.configuration.native_redirect_uri
|
|
31
|
+
@client.save!
|
|
32
|
+
visit authorization_endpoint_url(client: @client)
|
|
33
|
+
click_on 'Authorize'
|
|
34
|
+
|
|
35
|
+
access_grant_should_exist_for(@client, @resource_owner)
|
|
36
|
+
|
|
37
|
+
code = current_params['code']
|
|
38
|
+
expect(code).not_to be_nil
|
|
39
|
+
|
|
40
|
+
hashed_code = Doorkeeper::AccessGrant.secret_strategy.transform_secret code
|
|
41
|
+
expect(hashed_code).to eq Doorkeeper::AccessGrant.first.token
|
|
42
|
+
|
|
43
|
+
expect(code).not_to eq(hashed_code)
|
|
44
|
+
|
|
45
|
+
i_should_see 'Authorization code:'
|
|
46
|
+
i_should_see code
|
|
47
|
+
i_should_not_see hashed_code
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
24
51
|
scenario 'resource owner authorizes using test url' do
|
|
25
52
|
@client.redirect_uri = Doorkeeper.configuration.native_redirect_uri
|
|
26
53
|
@client.save!
|
|
@@ -71,6 +98,19 @@ feature 'Authorization Code Flow' do
|
|
|
71
98
|
should_have_json 'error', 'invalid_client'
|
|
72
99
|
end
|
|
73
100
|
|
|
101
|
+
scenario 'resource owner requests an access token with authorization code but without client id' do
|
|
102
|
+
visit authorization_endpoint_url(client: @client)
|
|
103
|
+
click_on 'Authorize'
|
|
104
|
+
|
|
105
|
+
authorization_code = Doorkeeper::AccessGrant.first.token
|
|
106
|
+
page.driver.post token_endpoint_url(code: authorization_code, client_secret: @client.secret,
|
|
107
|
+
redirect_uri: @client.redirect_uri)
|
|
108
|
+
|
|
109
|
+
expect(Doorkeeper::AccessToken.count).to be_zero
|
|
110
|
+
|
|
111
|
+
should_have_json 'error', 'invalid_client'
|
|
112
|
+
end
|
|
113
|
+
|
|
74
114
|
scenario 'silently authorizes if matching token exists' do
|
|
75
115
|
default_scopes_exist :public, :write
|
|
76
116
|
|
|
@@ -290,7 +330,7 @@ feature 'Authorization Code Flow' do
|
|
|
290
330
|
|
|
291
331
|
context 'when application scopes are present and no scope is passed' do
|
|
292
332
|
background do
|
|
293
|
-
@client.
|
|
333
|
+
@client.update(scopes: 'public write read')
|
|
294
334
|
end
|
|
295
335
|
|
|
296
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(
|
|
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.
|
|
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.
|
|
23
|
+
@client.update(scopes: 'public write read')
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
scenario 'access token has no scopes' do
|
|
@@ -64,7 +64,8 @@ describe 'Resource Owner Password Credentials Flow' do
|
|
|
64
64
|
)
|
|
65
65
|
end.not_to(change { Doorkeeper::AccessToken.count })
|
|
66
66
|
|
|
67
|
-
expect(response).
|
|
67
|
+
expect(response.status).to eq(401)
|
|
68
|
+
should_have_json 'error', 'invalid_client'
|
|
68
69
|
end
|
|
69
70
|
end
|
|
70
71
|
end
|
|
@@ -88,7 +89,8 @@ describe 'Resource Owner Password Credentials Flow' do
|
|
|
88
89
|
post password_token_endpoint_url(client_id: @client.uid, resource_owner: @resource_owner)
|
|
89
90
|
end.not_to(change { Doorkeeper::AccessToken.count })
|
|
90
91
|
|
|
91
|
-
expect(response).
|
|
92
|
+
expect(response.status).to eq(401)
|
|
93
|
+
should_have_json 'error', 'invalid_client'
|
|
92
94
|
end
|
|
93
95
|
end
|
|
94
96
|
end
|
|
@@ -147,7 +149,7 @@ describe 'Resource Owner Password Credentials Flow' do
|
|
|
147
149
|
context 'when application scopes are present and differs from configured default scopes and no scope is passed' do
|
|
148
150
|
before do
|
|
149
151
|
default_scopes_exist :public
|
|
150
|
-
@client.
|
|
152
|
+
@client.update(scopes: 'abc')
|
|
151
153
|
end
|
|
152
154
|
|
|
153
155
|
it 'issues new token without any scope' do
|
|
@@ -166,7 +168,7 @@ describe 'Resource Owner Password Credentials Flow' do
|
|
|
166
168
|
|
|
167
169
|
context 'when application scopes contain some of the default scopes and no scope is passed' do
|
|
168
170
|
before do
|
|
169
|
-
@client.
|
|
171
|
+
@client.update(scopes: 'read write public')
|
|
170
172
|
end
|
|
171
173
|
|
|
172
174
|
it 'issues new token with one default scope that are present in application scopes' do
|
|
@@ -215,7 +217,7 @@ describe 'Resource Owner Password Credentials Flow' do
|
|
|
215
217
|
should_have_json 'error_description', translated_error_message(:invalid_scope)
|
|
216
218
|
should_not_have_json 'access_token'
|
|
217
219
|
|
|
218
|
-
expect(response.status).to eq(
|
|
220
|
+
expect(response.status).to eq(400)
|
|
219
221
|
end
|
|
220
222
|
end
|
|
221
223
|
|