jwt_keeper 2.0.0

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.
@@ -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