jwt_keeper 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,188 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe JWTKeeper do
4
+ describe 'Controller' do
5
+ include_context 'initialize config'
6
+
7
+ let(:token) { JWTKeeper::Token.create(claim: "Jet fuel can't melt steel beams") }
8
+ subject(:test_controller) do
9
+ instance = Class.new do
10
+ attr_accessor :request, :response
11
+ include RSpec::Mocks::ExampleMethods
12
+ include JWTKeeper::Controller
13
+
14
+ def session
15
+ { return_to_url: 'http://www.example.com' }
16
+ end
17
+
18
+ def root_path
19
+ '/'
20
+ end
21
+
22
+ def regenerate_claims(_old_token)
23
+ { regenerate_claims: true }
24
+ end
25
+
26
+ def redirect_to(path, message = nil)
27
+ end
28
+ end.new
29
+
30
+ instance.request =
31
+ instance_double('Request', headers: { 'Authorization' => "Bearer #{token}" })
32
+ instance.response =
33
+ instance_double('Response', headers: {})
34
+ instance
35
+ end
36
+
37
+ describe '#included' do
38
+ it { is_expected.to respond_to(:require_authentication) }
39
+ it { is_expected.to respond_to(:authentication_token) }
40
+ it { is_expected.to respond_to(:authentication_token=) }
41
+ it { is_expected.to respond_to(:redirect_back_or_to) }
42
+ it { is_expected.to respond_to(:not_authenticated) }
43
+ it { is_expected.to respond_to(:authenticated) }
44
+ end
45
+
46
+ describe '#require_authentication' do
47
+ context 'with valid token' do
48
+ before do
49
+ allow(test_controller).to receive(:authenticated)
50
+ end
51
+
52
+ it 'calls authenticated' do
53
+ subject.require_authentication
54
+ expect(subject).to have_received(:authenticated).once
55
+ end
56
+
57
+ it 'does not rotates the token' do
58
+ expect { subject.require_authentication }.to_not change {
59
+ subject.authentication_token.id
60
+ }
61
+ end
62
+ end
63
+
64
+ context 'with expired token' do
65
+ let(:token) { JWTKeeper::Token.create(exp: 3.hours.ago) }
66
+ before do
67
+ allow(test_controller).to receive(:not_authenticated)
68
+ end
69
+
70
+ it 'calls not_authenticated' do
71
+ subject.require_authentication
72
+ expect(subject).to have_received(:not_authenticated).once
73
+ end
74
+ end
75
+
76
+ context 'with pending token' do
77
+ let(:token) do
78
+ token = JWTKeeper::Token.create({})
79
+ JWTKeeper::Token.rotate(token.id)
80
+ token
81
+ end
82
+ before(:each) do
83
+ allow(test_controller).to receive(:authenticated)
84
+ end
85
+
86
+ it 'calls authenticated' do
87
+ subject.require_authentication
88
+ expect(subject).to have_received(:authenticated).once
89
+ end
90
+
91
+ it 'rotates the token' do
92
+ expect { subject.require_authentication }.to change {
93
+ subject.authentication_token.id
94
+ }
95
+ end
96
+ end
97
+
98
+ context 'with version_mismatch token' do
99
+ let(:token) { JWTKeeper::Token.create(ver: 'mismatch') }
100
+ before(:each) do
101
+ allow(test_controller).to receive(:authenticated)
102
+ end
103
+
104
+ it 'calls authenticated' do
105
+ subject.require_authentication
106
+ expect(subject).to have_received(:authenticated).once
107
+ end
108
+
109
+ it 'rotates the token' do
110
+ expect { subject.require_authentication }.to change {
111
+ subject.authentication_token.id
112
+ }
113
+ end
114
+ end
115
+ end
116
+
117
+ describe '#regenerate_claims' do
118
+ let(:token) do
119
+ token = JWTKeeper::Token.create({})
120
+ JWTKeeper::Token.rotate(token.id)
121
+ token
122
+ end
123
+ before(:each) do
124
+ allow(test_controller).to receive(:authenticated)
125
+ end
126
+
127
+ it 'is used to update the token claims on rotation' do
128
+ expect(subject.authentication_token.claims[:regenerate_claims]).to be nil
129
+ expect { subject.require_authentication }.to change(subject, :authentication_token)
130
+ expect(subject.authentication_token.claims[:regenerate_claims]).to be true
131
+ end
132
+ end
133
+
134
+ describe '#respond_with_authentication' do
135
+ before do
136
+ subject.authentication_token = token
137
+ end
138
+
139
+ it 'sets the reponses token with the authentication_token' do
140
+ subject.respond_with_authentication
141
+ expect(subject.response.headers['Authorization']).to eq "Bearer #{token}"
142
+ end
143
+ end
144
+
145
+ describe '#authentication_token' do
146
+ context 'valid request in token' do
147
+ it 'returns the decoded token from the current request' do
148
+ expect(subject.authentication_token.claims[:claim]).to eq "Jet fuel can't melt steel beams"
149
+ end
150
+ end
151
+ context 'no token in request' do
152
+ before do
153
+ token = JWTKeeper::Token.create(exp: 3.hours.ago)
154
+ subject.request =
155
+ instance_double('Request', headers: { 'Authorization' => "Bearer #{token}" })
156
+ end
157
+
158
+ it 'returns nil' do
159
+ expect(subject.authentication_token).to be nil
160
+ end
161
+ end
162
+ end
163
+
164
+ describe '#redirect_back_or_to' do
165
+ let(:path) { 'http://www.example.com' }
166
+
167
+ before do
168
+ allow(test_controller).to receive(:redirect_to)
169
+ end
170
+
171
+ it 'it calls redirect_to' do
172
+ subject.redirect_back_or_to(path)
173
+ expect(subject).to have_received(:redirect_to).with(path, anything)
174
+ end
175
+ end
176
+
177
+ describe '#not_authenticated' do
178
+ before do
179
+ allow(test_controller).to receive(:redirect_to)
180
+ end
181
+
182
+ it 'it calls redirect_to' do
183
+ subject.not_authenticated
184
+ expect(subject).to have_received(:redirect_to).with('/')
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,70 @@
1
+ RSpec.describe JWTKeeper::Datastore do
2
+ include_context 'initialize config'
3
+ let(:jti) { SecureRandom.uuid }
4
+
5
+ describe '.rotate' do
6
+ before { described_class.rotate(jti, 30) }
7
+
8
+ it 'stores a token_id with a soft expiry' do
9
+ expect(described_class.send(:get, jti)).to eq 'soft'
10
+ end
11
+ end
12
+
13
+ describe '.pending?' do
14
+ context 'with a missing token' do
15
+ it 'returns false' do
16
+ expect(described_class.pending?(jti)).to be false
17
+ end
18
+ end
19
+
20
+ context 'with a revoked token' do
21
+ before { described_class.revoke(jti, 30) }
22
+
23
+ it 'returns false' do
24
+ expect(described_class.pending?(jti)).to be false
25
+ end
26
+ end
27
+
28
+ context 'with a pending token' do
29
+ before { described_class.rotate(jti, 30) }
30
+
31
+ it 'returns true' do
32
+ expect(described_class.pending?(jti)).to be true
33
+ end
34
+ end
35
+ end
36
+
37
+ describe '.revoke' do
38
+ before do
39
+ described_class.revoke(jti, 30)
40
+ end
41
+
42
+ it 'stores a token_id with a hard expiry' do
43
+ expect(described_class.send(:get, jti)).to eq 'hard'
44
+ end
45
+ end
46
+
47
+ describe '.revoked?' do
48
+ context 'with a missing token' do
49
+ it 'returns false' do
50
+ expect(described_class.revoked?(jti)).to be false
51
+ end
52
+ end
53
+
54
+ context 'with a revoked token' do
55
+ before { described_class.revoke(jti, 30) }
56
+
57
+ it 'returns true' do
58
+ expect(described_class.revoked?(jti)).to be true
59
+ end
60
+ end
61
+
62
+ context 'with a pending token' do
63
+ before { described_class.rotate(jti, 30) }
64
+
65
+ it 'returns false' do
66
+ expect(described_class.revoked?(jti)).to be false
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,180 @@
1
+ require 'spec_helper'
2
+
3
+ module JWTKeeper
4
+ RSpec.describe Token do
5
+ include_context 'initialize config'
6
+ let(:private_claims) { { claim: "Jet fuel can't melt steel beams" } }
7
+ let(:raw_token) { described_class.create(private_claims).to_jwt }
8
+
9
+ describe '.create' do
10
+ subject { described_class.create(private_claims) }
11
+
12
+ it { is_expected.to be_instance_of described_class }
13
+ it { expect(subject.claims[:claim]).to eql private_claims[:claim] }
14
+ end
15
+
16
+ describe '.find' do
17
+ subject { described_class.find(raw_token) }
18
+
19
+ it { is_expected.to be_instance_of described_class }
20
+ it { expect(subject.claims[:claim]).to eql private_claims[:claim] }
21
+
22
+ context 'with invalid token' do
23
+ let(:private_claims) { { exp: 1.hour.ago } }
24
+
25
+ it { is_expected.to be nil }
26
+ end
27
+
28
+ context 'with revoked token' do
29
+ before { described_class.find(raw_token).revoke }
30
+
31
+ it { is_expected.to be nil }
32
+ end
33
+ end
34
+
35
+ describe '.rotate' do
36
+ subject(:token) { described_class.create(private_claims) }
37
+ before(:each) { described_class.rotate(token.id) }
38
+
39
+ it 'marks the token for rotation' do
40
+ expect(token.pending?).to eq true
41
+ end
42
+ end
43
+
44
+ describe '.revoke' do
45
+ subject(:token) { described_class.create(private_claims) }
46
+
47
+ it 'invalidates the token' do
48
+ expect(token.valid?).to eq true
49
+ expect(token.revoked?).to eq false
50
+
51
+ expect(described_class.revoke(token.claims[:jti]))
52
+
53
+ expect(token.valid?).to eq false
54
+ expect(token.revoked?).to eq true
55
+ end
56
+ end
57
+
58
+ describe '#revoke' do
59
+ subject(:token) { described_class.create(private_claims) }
60
+
61
+ it 'invalidates the token' do
62
+ expect(token.valid?).to eq true
63
+ expect(token.revoked?).to eq false
64
+
65
+ expect(token.revoke)
66
+
67
+ expect(token.valid?).to eq false
68
+ expect(token.revoked?).to eq true
69
+ end
70
+ end
71
+
72
+ describe '#revoked?' do
73
+ subject(:token) { described_class.create(private_claims) }
74
+
75
+ context 'with a revoked token' do
76
+ before { token.revoke }
77
+
78
+ it { is_expected.to be_revoked }
79
+ end
80
+
81
+ context 'with a pending token' do
82
+ before { described_class.rotate(token.id) }
83
+
84
+ it { is_expected.not_to be_revoked }
85
+ end
86
+
87
+ context 'with a valid token' do
88
+ it { is_expected.not_to be_revoked }
89
+ end
90
+ end
91
+
92
+ describe '#pending?' do
93
+ subject(:token) { described_class.create(private_claims) }
94
+
95
+ context 'with a revoked token' do
96
+ before { token.revoke }
97
+
98
+ it { is_expected.not_to be_pending }
99
+ end
100
+
101
+ context 'with a config pending token' do
102
+ before { token.claims[:ver] = 'version' }
103
+
104
+ it { is_expected.to_not be_pending }
105
+ end
106
+
107
+ context 'with a redis pending token' do
108
+ before { described_class.rotate(token.id) }
109
+
110
+ it { is_expected.to be_pending }
111
+ end
112
+
113
+ context 'with a valid token' do
114
+ it { is_expected.not_to be_pending }
115
+ end
116
+ end
117
+
118
+ describe '#version_mismatch?' do
119
+ subject(:token) { described_class.create(private_claims) }
120
+
121
+ context 'with a revoked token' do
122
+ before { token.revoke }
123
+
124
+ it { is_expected.not_to be_version_mismatch }
125
+ end
126
+
127
+ context 'with a config pending token' do
128
+ before { token.claims[:ver] = 'version' }
129
+
130
+ it { is_expected.to be_version_mismatch }
131
+ end
132
+
133
+ context 'with a redis pending token' do
134
+ before { described_class.rotate(token.id) }
135
+
136
+ it { is_expected.to_not be_version_mismatch }
137
+ end
138
+
139
+ context 'with a valid token' do
140
+ it { is_expected.not_to be_version_mismatch }
141
+ end
142
+ end
143
+
144
+ describe '#rotate' do
145
+ let(:old_token) { described_class.create(private_claims) }
146
+ let(:new_token) { old_token.dup.rotate }
147
+ before { new_token }
148
+
149
+ it { expect(old_token).to be_invalid }
150
+ it { expect(new_token).to be_valid }
151
+ it { expect(old_token.claims[:claim]).to eq new_token.claims[:claim] }
152
+ end
153
+
154
+ describe '#valid?' do
155
+ subject { described_class.create(private_claims) }
156
+
157
+ context 'when invalid' do
158
+ before { JWTKeeper.configure(JWTKeeper::Configuration.new(test_config.merge(expiry: -1.hours))) }
159
+ it { is_expected.not_to be_valid }
160
+ end
161
+
162
+ context 'when valid' do
163
+ it { is_expected.to be_valid }
164
+ end
165
+ end
166
+
167
+ describe '#invalid?' do
168
+ subject { described_class.create(private_claims) }
169
+
170
+ context 'when invalid' do
171
+ before { JWTKeeper.configure(JWTKeeper::Configuration.new(test_config.merge(expiry: -1.hours))) }
172
+ it { is_expected.to be_invalid }
173
+ end
174
+
175
+ context 'when valid' do
176
+ it { is_expected.not_to be_invalid }
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe JWTKeeper do
4
+ describe '#configure' do
5
+ let(:test_config) do
6
+ {
7
+ algorithm: 'HS256',
8
+ secret: 'secret',
9
+ expiry: 24.hours,
10
+ issuer: 'api.example.com',
11
+ audience: 'example.com',
12
+ redis_connection: Redis.new(url: ENV['REDIS_URL'])
13
+ }
14
+ end
15
+
16
+ context 'without block' do
17
+ before do
18
+ described_class.configure(JWTKeeper::Configuration.new(test_config))
19
+ end
20
+
21
+ it 'sets the configuration based on param' do
22
+ expect(described_class.configuration.secret).to eql test_config[:secret]
23
+ end
24
+ end
25
+
26
+ context 'with block' do
27
+ before do
28
+ described_class.configure do |config|
29
+ config.secret = test_config[:secret]
30
+ end
31
+ end
32
+
33
+ it 'sets configuration based on the block' do
34
+ expect(described_class.configuration.secret).to eql test_config[:secret]
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,58 @@
1
+ require 'dotenv'
2
+ Dotenv.load
3
+
4
+ require 'simplecov'
5
+ require 'codeclimate-test-reporter'
6
+
7
+ SimpleCov.formatter =
8
+ SimpleCov::Formatter::MultiFormatter.new([
9
+ SimpleCov::Formatter::HTMLFormatter,
10
+ CodeClimate::TestReporter::Formatter
11
+ ])
12
+ SimpleCov.start
13
+
14
+ require 'rails'
15
+ require 'jwt_keeper'
16
+
17
+ RSpec.configure do |config|
18
+ config.expect_with :rspec do |expectations|
19
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
20
+ end
21
+
22
+ config.mock_with :rspec do |mocks|
23
+ mocks.verify_partial_doubles = true
24
+ end
25
+
26
+ config.filter_run :focus
27
+ config.run_all_when_everything_filtered = true
28
+ config.example_status_persistence_file_path = 'spec/examples.txt'
29
+ config.disable_monkey_patching!
30
+
31
+ config.default_formatter = 'doc' if config.files_to_run.one?
32
+
33
+ # config.profile_examples = 10
34
+
35
+ # Run specs in random order to surface order dependencies. If you find an
36
+ # order dependency and want to debug it, you can fix the order by providing
37
+ # the seed, which is printed after each run.
38
+ # --seed 1234
39
+ config.order = :random
40
+ Kernel.srand config.seed
41
+ end
42
+
43
+ RSpec.shared_context 'initialize config' do
44
+ let(:test_config) do
45
+ {
46
+ algorithm: 'HS256',
47
+ secret: 'secret',
48
+ expiry: 24.hours,
49
+ issuer: 'api.example.com',
50
+ audience: 'example.com',
51
+ redis_connection: Redis.new(url: ENV['REDIS_URL'])
52
+ }
53
+ end
54
+
55
+ before(:each) do
56
+ JWTKeeper.configure(JWTKeeper::Configuration.new(test_config))
57
+ end
58
+ end