signinable 2.0.12 → 2.0.13
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 +4 -4
- data/app/models/signin.rb +16 -5
- data/lib/signinable/engine.rb +2 -0
- data/lib/signinable/model_additions.rb +64 -24
- data/lib/signinable/version.rb +1 -1
- data/spec/dummy/app/models/user.rb +1 -1
- data/spec/dummy/log/test.log +11831 -0
- data/spec/models/signin_spec.rb +82 -32
- data/spec/models/user_spec.rb +181 -60
- data/spec/support/utilities.rb +1 -2
- metadata +16 -2
data/spec/models/signin_spec.rb
CHANGED
|
@@ -3,50 +3,100 @@
|
|
|
3
3
|
require 'rails_helper'
|
|
4
4
|
|
|
5
5
|
describe Signin do
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
end
|
|
6
|
+
describe '#expired?' do
|
|
7
|
+
let(:expiration_time) { Time.zone.now + 1.hour }
|
|
8
|
+
let(:signin) { create(:signin, expiration_time: expiration_time) }
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
signin = create(:signin)
|
|
14
|
-
signin.token = nil
|
|
15
|
-
expect(signin).to_not be_valid
|
|
10
|
+
it 'returns false when not expired' do
|
|
11
|
+
expect(signin).to_not be_expired
|
|
16
12
|
end
|
|
17
13
|
|
|
18
|
-
it '
|
|
19
|
-
|
|
14
|
+
it 'returns true when expired' do
|
|
15
|
+
Timecop.travel(expiration_time) do
|
|
16
|
+
expect(signin).to be_expired
|
|
17
|
+
end
|
|
20
18
|
end
|
|
21
19
|
end
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
context 'not valid with' do
|
|
29
|
-
it 'wrong ip' do
|
|
30
|
-
expect(build(:signin, ip: '123')).to_not be_valid
|
|
21
|
+
describe '#expireable?' do
|
|
22
|
+
it 'returns false when expireable' do
|
|
23
|
+
signin = create(:signin, expiration_time: nil)
|
|
24
|
+
expect(signin).to_not be_expireable
|
|
31
25
|
end
|
|
32
|
-
end
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Timecop.travel(expiration_time)
|
|
39
|
-
expect(signin).to be_expired
|
|
40
|
-
Timecop.return
|
|
27
|
+
it 'returns true when expireable' do
|
|
28
|
+
signin = create(:signin, expiration_time: Time.zone.now)
|
|
29
|
+
expect(signin).to be_expireable
|
|
30
|
+
end
|
|
41
31
|
end
|
|
42
32
|
|
|
43
|
-
describe '
|
|
44
|
-
it '
|
|
45
|
-
Timecop.freeze
|
|
33
|
+
describe '#expire!' do
|
|
34
|
+
it 'sets expiration_time to now' do
|
|
46
35
|
signin = create(:signin, expiration_time: (Time.zone.now + 1.hour))
|
|
36
|
+
allow(signin).to receive(:renew!)
|
|
47
37
|
signin.expire!
|
|
48
|
-
expect(signin).to
|
|
49
|
-
|
|
38
|
+
expect(signin).to have_received(:renew!).with(period: 0, ip: signin.ip, user_agent: signin.user_agent)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#renew!' do
|
|
43
|
+
let(:signin) { create(:signin) }
|
|
44
|
+
let(:attrs) do
|
|
45
|
+
{
|
|
46
|
+
period: 100,
|
|
47
|
+
ip: signin.ip,
|
|
48
|
+
user_agent: signin.user_agent,
|
|
49
|
+
refresh_token: false
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
before(:each) do
|
|
54
|
+
allow(signin).to receive(:update!)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'updates ip and user_agent' do
|
|
58
|
+
signin.renew!(**attrs)
|
|
59
|
+
expect(signin).to have_received(:update!).with(hash_including(ip: signin.ip, user_agent: signin.user_agent))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context 'when expireable' do
|
|
63
|
+
before(:each) do
|
|
64
|
+
allow(signin).to receive(:expireable?).and_return(true)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'updates expiration_time' do
|
|
68
|
+
Timecop.freeze do
|
|
69
|
+
signin.renew!(**attrs)
|
|
70
|
+
expect(signin).to have_received(:update!).with(hash_including(expiration_time: Time.zone.now + attrs[:period]))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'when not expireable' do
|
|
76
|
+
before(:each) do
|
|
77
|
+
allow(signin).to receive(:expireable?).and_return(false)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'does not update expiration_time' do
|
|
81
|
+
signin.renew!(**attrs)
|
|
82
|
+
expect(signin).to have_received(:update!).with(hash_excluding(expiration_time: Time.zone.now + attrs[:period]))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
context 'when need to refresh_token' do
|
|
87
|
+
it 'updates expiration_time' do
|
|
88
|
+
allow(SecureRandom).to receive(:urlsafe_base64).and_return('bla')
|
|
89
|
+
signin.renew!(**attrs.merge(refresh_token: true))
|
|
90
|
+
expect(signin).to have_received(:update!).with(hash_including(token: 'bla'))
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
context 'when no need to refresh_token' do
|
|
95
|
+
it 'does not update expiration_time' do
|
|
96
|
+
allow(SecureRandom).to receive(:urlsafe_base64).and_return('bla')
|
|
97
|
+
signin.renew!(**attrs)
|
|
98
|
+
expect(signin).to have_received(:update!).with(hash_excluding(token: 'bla'))
|
|
99
|
+
end
|
|
50
100
|
end
|
|
51
101
|
end
|
|
52
102
|
end
|
data/spec/models/user_spec.rb
CHANGED
|
@@ -15,118 +15,239 @@ describe User do
|
|
|
15
15
|
Timecop.return
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
describe '
|
|
18
|
+
describe '#signin' do
|
|
19
19
|
it 'should create Signin' do
|
|
20
20
|
expect do
|
|
21
21
|
sign_in_user(user, credentials)
|
|
22
22
|
end.to change(Signin, :count).by(1)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
it 'sets jwt on user instance' do
|
|
26
|
+
expect do
|
|
27
|
+
sign_in_user(user, credentials)
|
|
28
|
+
end.to change(user, :jwt).from(nil)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'should generate jwt with correct payload' do
|
|
32
|
+
sign_in_user(user, credentials)
|
|
33
|
+
signin = user.last_signin
|
|
34
|
+
payload = JWT.decode(user.jwt, 'test', true, { algorithm: 'HS256' })[0]
|
|
35
|
+
expect(payload).to include(
|
|
36
|
+
'refresh_token' => signin.token,
|
|
37
|
+
'signinable_id' => user.id
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
25
41
|
it 'should set expiration_time' do
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
sign_in_user(user, credentials)
|
|
43
|
+
signin = user.last_signin
|
|
44
|
+
expect(signin.expiration_time.to_i).to eq((Time.zone.now + User.refresh_exp).to_i)
|
|
28
45
|
end
|
|
29
46
|
|
|
30
47
|
it 'should not set expiration_time' do
|
|
31
|
-
allow(
|
|
32
|
-
|
|
48
|
+
allow(described_class).to receive(:refresh_exp).and_return(0)
|
|
49
|
+
sign_in_user(user, credentials)
|
|
50
|
+
signin = user.last_signin
|
|
33
51
|
expect(signin.expiration_time).to be_nil
|
|
34
52
|
end
|
|
53
|
+
|
|
54
|
+
context 'when simultaneous signins enabled' do
|
|
55
|
+
before do
|
|
56
|
+
allow(described_class).to receive(:simultaneous_signings).and_return(true)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'does not expire active signins' do
|
|
60
|
+
sign_in_user(user, credentials)
|
|
61
|
+
sign_in_user(user, credentials)
|
|
62
|
+
expect(user.signins.active.count).to eq(2)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context 'when simultaneous signins disabled' do
|
|
67
|
+
before do
|
|
68
|
+
allow(described_class).to receive(:simultaneous_signings).and_return(false)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'expires active signins' do
|
|
72
|
+
sign_in_user(user, credentials)
|
|
73
|
+
sign_in_user(user, credentials)
|
|
74
|
+
expect(user.signins.active.count).to eq(1)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
35
77
|
end
|
|
36
78
|
|
|
37
|
-
describe '
|
|
79
|
+
describe '#signout' do
|
|
38
80
|
it 'should expire signin' do
|
|
39
|
-
|
|
81
|
+
sign_in_user(user, credentials)
|
|
82
|
+
signin = user.last_signin
|
|
40
83
|
sign_out_user(signin, credentials)
|
|
41
84
|
expect(signin.reload).to be_expired
|
|
42
85
|
end
|
|
43
86
|
|
|
44
|
-
context '
|
|
87
|
+
context 'when has no restrictions' do
|
|
45
88
|
%i[ip user_agent].each do |c|
|
|
46
|
-
it "
|
|
47
|
-
|
|
89
|
+
it "allows signout when #{c} changes" do
|
|
90
|
+
sign_in_user(user, credentials)
|
|
91
|
+
signin = user.last_signin
|
|
48
92
|
expect(sign_out_user(signin, credentials)).to be_truthy
|
|
49
93
|
end
|
|
50
94
|
end
|
|
51
95
|
end
|
|
52
96
|
|
|
53
|
-
context '
|
|
97
|
+
context 'when has restrictions' do
|
|
54
98
|
%i[ip user_agent].each do |c|
|
|
55
|
-
it "
|
|
56
|
-
allow(
|
|
57
|
-
|
|
99
|
+
it "forbids signout when #{c} changes" do
|
|
100
|
+
allow(described_class).to receive(:signin_restrictions).and_return([c])
|
|
101
|
+
sign_in_user(user, credentials)
|
|
102
|
+
signin = user.last_signin
|
|
58
103
|
expect(sign_out_user(signin, other_credentials)).to be_nil
|
|
59
104
|
end
|
|
60
105
|
end
|
|
61
106
|
end
|
|
62
107
|
end
|
|
63
108
|
|
|
64
|
-
describe '#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
109
|
+
describe '#last_signin' do
|
|
110
|
+
it 'retuns nil when no signins' do
|
|
111
|
+
expect(user.last_signin).to be_nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'returns last active signin' do
|
|
115
|
+
sign_in_user(user, credentials)
|
|
116
|
+
sign_in_user(user, credentials)
|
|
117
|
+
signin = user.signins.active.last
|
|
118
|
+
sign_in_user(user, credentials)
|
|
119
|
+
user.signins.last.expire!
|
|
120
|
+
|
|
121
|
+
expect(user.last_signin).to eq(signin)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe '.generate_jwt' do
|
|
126
|
+
let(:token) { SecureRandom.urlsafe_base64(rand(50..100)) }
|
|
127
|
+
|
|
128
|
+
it 'sets correct payload' do
|
|
129
|
+
jwt = described_class.generate_jwt(token, user.id)
|
|
130
|
+
payload = JWT.decode(jwt, described_class.jwt_secret, true, { algorithm: 'HS256' })[0]
|
|
131
|
+
expect(payload).to eq(
|
|
132
|
+
'refresh_token' => token,
|
|
133
|
+
'signinable_id' => user.id,
|
|
134
|
+
'exp' => Time.zone.now.to_i + described_class.jwt_exp
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe '.authenticate_with_token' do
|
|
140
|
+
context 'when jwt is invalid' do
|
|
141
|
+
it 'returns nil' do
|
|
142
|
+
expect(described_class.authenticate_with_token('blablabla', *credentials)).to be_nil
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
context 'when jwt has not expired' do
|
|
147
|
+
before(:each) do
|
|
148
|
+
sign_in_user(user, credentials)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'returns user' do
|
|
152
|
+
expect(described_class.authenticate_with_token(user.jwt, *credentials)).to eq(user)
|
|
74
153
|
end
|
|
75
154
|
|
|
76
|
-
it '
|
|
77
|
-
allow(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
Timecop.travel(Time.zone.now + 1.hour)
|
|
81
|
-
User.authenticate_with_token(signin.token, *credentials)
|
|
82
|
-
signin.reload
|
|
83
|
-
expect(signin.expiration_time.to_i).to eq(old_time.to_i)
|
|
155
|
+
it 'does not update refresh token' do
|
|
156
|
+
allow(described_class).to receive(:refresh_jwt)
|
|
157
|
+
described_class.authenticate_with_token(user.jwt, *credentials)
|
|
158
|
+
expect(described_class).not_to have_received(:refresh_jwt)
|
|
84
159
|
end
|
|
85
160
|
end
|
|
86
161
|
|
|
87
|
-
context '
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
expect(User.authenticate_with_token(signin1.token, *credentials)).to eq(user)
|
|
92
|
-
expect(User.authenticate_with_token(signin2.token, *credentials)).to eq(user)
|
|
162
|
+
context 'when jwt has expired' do
|
|
163
|
+
before(:each) do
|
|
164
|
+
sign_in_user(user, credentials)
|
|
165
|
+
Timecop.travel(Time.zone.now + described_class.jwt_exp)
|
|
93
166
|
end
|
|
94
167
|
|
|
95
|
-
it '
|
|
96
|
-
|
|
97
|
-
|
|
168
|
+
it 'does not do user lookup' do
|
|
169
|
+
allow(described_class).to receive(:find_by)
|
|
170
|
+
described_class.authenticate_with_token(user.jwt, *credentials)
|
|
171
|
+
expect(described_class).not_to have_received(:find_by)
|
|
98
172
|
end
|
|
99
173
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
end
|
|
174
|
+
it 'calls for refresh token' do
|
|
175
|
+
allow(described_class).to receive(:refresh_jwt)
|
|
176
|
+
described_class.authenticate_with_token(user.jwt, *credentials)
|
|
177
|
+
expect(described_class).to have_received(:refresh_jwt)
|
|
105
178
|
end
|
|
106
179
|
end
|
|
180
|
+
end
|
|
107
181
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
signin2 = sign_in_user(user, credentials)
|
|
113
|
-
expect(User.authenticate_with_token(signin1.token, *credentials)).to be_nil
|
|
114
|
-
expect(User.authenticate_with_token(signin2.token, *credentials)).to eq(user)
|
|
182
|
+
describe '.refresh_jwt' do
|
|
183
|
+
context 'when jwt is invalid' do
|
|
184
|
+
it 'returns nil' do
|
|
185
|
+
expect(described_class.refresh_jwt('blablabla', *credentials)).to be_nil
|
|
115
186
|
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it 'returns nil when signin not found' do
|
|
190
|
+
jwt = described_class.generate_jwt('blablabla', 0)
|
|
191
|
+
expect(described_class.refresh_jwt(jwt, *credentials)).to be_nil
|
|
192
|
+
end
|
|
116
193
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
194
|
+
it 'returns nil when signin expired' do
|
|
195
|
+
sign_in_user(user, credentials)
|
|
196
|
+
signin = user.last_signin
|
|
197
|
+
Timecop.travel(Time.zone.now + described_class.refresh_exp)
|
|
198
|
+
expect(described_class.refresh_jwt(user.jwt, *credentials)).to be_nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
context 'when has no restrictions' do
|
|
202
|
+
%i[ip user_agent].each do |c|
|
|
203
|
+
it "allows signin when #{c} changed" do
|
|
204
|
+
sign_in_user(user, credentials)
|
|
205
|
+
expect(described_class.refresh_jwt(user.jwt, *other_credentials)).to eq(user)
|
|
206
|
+
end
|
|
121
207
|
end
|
|
208
|
+
end
|
|
122
209
|
|
|
210
|
+
context 'when has restrictions' do
|
|
123
211
|
%i[ip user_agent].each do |c|
|
|
124
|
-
it "
|
|
212
|
+
it "forbids signin when #{c} changed" do
|
|
125
213
|
allow(User).to receive(:signin_restrictions).and_return([c])
|
|
126
|
-
|
|
127
|
-
expect(
|
|
214
|
+
sign_in_user(user, credentials)
|
|
215
|
+
expect(described_class.refresh_jwt(user.jwt, *other_credentials)).to be_nil
|
|
128
216
|
end
|
|
129
217
|
end
|
|
130
218
|
end
|
|
219
|
+
|
|
220
|
+
it 'renews signin' do
|
|
221
|
+
sign_in_user(user, credentials)
|
|
222
|
+
signin = user.last_signin
|
|
223
|
+
allow(signin).to receive(:renew!)
|
|
224
|
+
allow(Signin).to receive(:find_by).with(token: signin.token).and_return(signin)
|
|
225
|
+
|
|
226
|
+
described_class.refresh_jwt(user.jwt, *credentials)
|
|
227
|
+
expect(signin).to have_received(:renew!).with(period: described_class.expiration_period, ip: credentials[0],
|
|
228
|
+
user_agent: credentials[1], refresh_token: true)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it 'assigns new jwt' do
|
|
232
|
+
sign_in_user(user, credentials)
|
|
233
|
+
signin = user.last_signin
|
|
234
|
+
allow(user).to receive(:jwt=)
|
|
235
|
+
allow(signin).to receive(:signinable).and_return(user)
|
|
236
|
+
allow(Signin).to receive(:find_by).with(token: signin.token).and_return(signin)
|
|
237
|
+
allow(described_class).to receive(:generate_jwt).and_return('bla')
|
|
238
|
+
|
|
239
|
+
described_class.refresh_jwt(user.jwt, *credentials)
|
|
240
|
+
expect(user).to have_received(:jwt=).with('bla')
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'regenerates jwt' do
|
|
244
|
+
sign_in_user(user, credentials)
|
|
245
|
+
signin = user.last_signin
|
|
246
|
+
allow(described_class).to receive(:generate_jwt)
|
|
247
|
+
|
|
248
|
+
described_class.refresh_jwt(user.jwt, *credentials)
|
|
249
|
+
signin.reload
|
|
250
|
+
expect(described_class).to have_received(:generate_jwt).with(signin.token, signin.signinable_id)
|
|
251
|
+
end
|
|
131
252
|
end
|
|
132
253
|
end
|
data/spec/support/utilities.rb
CHANGED
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: signinable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ivan Novozhenets
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-06-
|
|
11
|
+
date: 2022-06-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: jwt
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 2.4.1
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 2.4.1
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: rails
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|