signinable 2.0.12 → 2.0.13
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e090b91cbbcbbfe860ca07984e3a19a965cc96bf2909f4e469e5d0ed48794dbe
|
4
|
+
data.tar.gz: c84bacb35618b62991ab60c984ec8f8c9786d783c3dd3dc8dc6a9ea76ed4881e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3e96a9511dd6560c05ddfdc03b94d71ea83720e982526412a59a8e4a0461d475a4fb970ceb680636f23f144cb2273a811a0014897bf77597eeff7c7652d7e962
|
7
|
+
data.tar.gz: 2dcc381256505cebdeb990ac0340200c3134c8e1337f1bbe41082e233d35187ecdc4e30aebf71d316ea99da7de861649415514d9043eaf958df59b630c38d439
|
data/app/models/signin.rb
CHANGED
@@ -10,12 +10,16 @@ class Signin < ActiveRecord::Base
|
|
10
10
|
presence: true,
|
11
11
|
format: { with: Regexp.union(Resolv::IPv4::Regex, Resolv::IPv6::Regex) }
|
12
12
|
|
13
|
+
scope :active, -> { where('expiration_time IS NULL OR expiration_time > ?', Time.zone.now) }
|
14
|
+
|
13
15
|
before_validation on: :create do
|
14
|
-
self.token =
|
16
|
+
self.token = generate_token
|
15
17
|
end
|
16
18
|
|
19
|
+
serialize :custom_data if ActiveRecord::Base.connection.instance_values['config'][:adapter].match('mysql')
|
20
|
+
|
17
21
|
def expire!
|
18
|
-
renew!(0)
|
22
|
+
renew!(period: 0, ip: ip, user_agent: user_agent)
|
19
23
|
end
|
20
24
|
|
21
25
|
def expired?
|
@@ -26,9 +30,16 @@ class Signin < ActiveRecord::Base
|
|
26
30
|
!expiration_time.nil?
|
27
31
|
end
|
28
32
|
|
29
|
-
def renew!(period)
|
30
|
-
|
33
|
+
def renew!(period:, ip:, user_agent:, refresh_token: false)
|
34
|
+
update_hash = { ip: ip, user_agent: user_agent }
|
35
|
+
update_hash[:expiration_time] = Time.zone.now + period if expireable?
|
36
|
+
update_hash[:token] = generate_token if refresh_token
|
37
|
+
update!(update_hash)
|
31
38
|
end
|
32
39
|
|
33
|
-
|
40
|
+
private
|
41
|
+
|
42
|
+
def generate_token
|
43
|
+
SecureRandom.urlsafe_base64(rand(50..100))
|
44
|
+
end
|
34
45
|
end
|
data/lib/signinable/engine.rb
CHANGED
@@ -6,30 +6,32 @@ module Signinable
|
|
6
6
|
|
7
7
|
module ClassMethods
|
8
8
|
ALLOWED_RESTRICTIONS = %i[ip user_agent].freeze
|
9
|
-
|
9
|
+
DEFAULT_REFRESH_EXP = 7200
|
10
|
+
DEFAULT_JWT_EXP = 900
|
10
11
|
|
11
|
-
cattr_reader :
|
12
|
+
cattr_reader :refresh_exp
|
12
13
|
cattr_reader :simultaneous_signings
|
13
14
|
cattr_reader :signin_restrictions
|
15
|
+
cattr_reader :jwt_secret
|
16
|
+
cattr_reader :jwt_exp
|
14
17
|
|
15
18
|
def signinable(options = {})
|
16
|
-
self.
|
19
|
+
self.refresh_exp = options.fetch(:expiration, DEFAULT_REFRESH_EXP)
|
17
20
|
self.simultaneous_signings = options.fetch(:simultaneous, true)
|
18
21
|
self.signin_restrictions = options[:restrictions]
|
22
|
+
self.jwt_secret = options.fetch(:jwt_secret)
|
23
|
+
self.jwt_exp = options.fetch(:jwt_exp, DEFAULT_JWT_EXP)
|
19
24
|
|
20
25
|
has_many :signins, as: :signinable, dependent: :destroy
|
26
|
+
|
27
|
+
attr_accessor :jwt
|
21
28
|
end
|
22
29
|
|
23
|
-
def authenticate_with_token(
|
24
|
-
|
30
|
+
def authenticate_with_token(jwt, ip, user_agent, skip_restrictions: [])
|
31
|
+
jwt_payload = extract_jwt_payload(jwt)
|
32
|
+
return refresh_jwt(jwt, ip, user_agent, skip_restrictions: skip_restrictions) unless jwt_payload
|
25
33
|
|
26
|
-
|
27
|
-
return nil if signin.expired?
|
28
|
-
return nil unless check_simultaneous_signings(signin)
|
29
|
-
return nil unless check_signin_permission(signin, { ip: ip, user_agent: user_agent }, skip_restrictions)
|
30
|
-
|
31
|
-
signin.renew!(expiration_period) if signin.expireable?
|
32
|
-
signin.signinable
|
34
|
+
find_by(primary_key => jwt_payload['signinable_id'])
|
33
35
|
end
|
34
36
|
|
35
37
|
def check_signin_permission(signin, restrictions_to_check, skip_restrictions)
|
@@ -37,16 +39,57 @@ module Signinable
|
|
37
39
|
end
|
38
40
|
|
39
41
|
def expiration_period
|
40
|
-
return
|
42
|
+
return refresh_exp.call if refresh_exp.respond_to?(:call)
|
43
|
+
|
44
|
+
refresh_exp
|
45
|
+
end
|
46
|
+
|
47
|
+
def generate_jwt(refresh_token, signinable_id)
|
48
|
+
JWT.encode(
|
49
|
+
{
|
50
|
+
refresh_token: refresh_token,
|
51
|
+
signinable_id: signinable_id,
|
52
|
+
exp: Time.zone.now.to_i + jwt_exp
|
53
|
+
},
|
54
|
+
jwt_secret,
|
55
|
+
'HS256'
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def refresh_jwt(jwt, ip, user_agent, skip_restrictions: [])
|
60
|
+
token = refresh_token_from_jwt(jwt)
|
61
|
+
return nil unless token
|
62
|
+
|
63
|
+
signin = Signin.find_by(token: token)
|
41
64
|
|
42
|
-
|
65
|
+
return unless signin
|
66
|
+
return nil if signin.expired?
|
67
|
+
return nil unless check_signin_permission(signin, { ip: ip, user_agent: user_agent }, skip_restrictions)
|
68
|
+
|
69
|
+
signin.renew!(period: expiration_period, ip: ip, user_agent: user_agent, refresh_token: true)
|
70
|
+
signin.signinable.jwt = generate_jwt(signin.token, signin.signinable_id)
|
71
|
+
signin.signinable
|
43
72
|
end
|
44
73
|
|
45
74
|
private
|
46
75
|
|
47
|
-
cattr_writer :
|
76
|
+
cattr_writer :refresh_exp
|
48
77
|
cattr_writer :simultaneous_signings
|
49
78
|
cattr_writer :signin_restrictions
|
79
|
+
cattr_writer :jwt_secret
|
80
|
+
cattr_writer :jwt_exp
|
81
|
+
|
82
|
+
def extract_jwt_payload(jwt)
|
83
|
+
JWT.decode(jwt, jwt_secret, true, { algorithm: 'HS256' })[0]
|
84
|
+
rescue JWT::DecodeError
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def refresh_token_from_jwt(jwt)
|
89
|
+
JWT.decode(jwt, jwt_secret, true, { verify_expiration: false, algorithm: 'HS256' })[0]['refresh_token']
|
90
|
+
rescue JWT::DecodeError
|
91
|
+
nil
|
92
|
+
end
|
50
93
|
|
51
94
|
def signin_permitted?(signin, restrictions_to_check, skip_restrictions)
|
52
95
|
restriction_fields = signin_restriction_fields(signin, skip_restrictions)
|
@@ -68,25 +111,22 @@ module Signinable
|
|
68
111
|
end
|
69
112
|
(fields - skip_restrictions) & ALLOWED_RESTRICTIONS
|
70
113
|
end
|
71
|
-
|
72
|
-
def check_simultaneous_signings(signin)
|
73
|
-
return true if simultaneous_signings
|
74
|
-
|
75
|
-
signin == signin.signinable.last_signin
|
76
|
-
end
|
77
114
|
end
|
78
115
|
|
79
116
|
def signin(ip, user_agent, referer, permanent: false, custom_data: {})
|
80
117
|
expires_in = self.class.expiration_period
|
81
118
|
expiration_time = expires_in.zero? || permanent ? nil : expires_in.seconds.from_now
|
82
|
-
Signin.
|
119
|
+
Signin.where(signinable: self).active.map(&:expire!) unless self.class.simultaneous_signings
|
120
|
+
signin = Signin.create!(
|
83
121
|
signinable: self,
|
84
122
|
ip: ip,
|
85
123
|
referer: referer,
|
86
124
|
user_agent: user_agent,
|
87
125
|
expiration_time: expiration_time,
|
88
126
|
custom_data: custom_data
|
89
|
-
)
|
127
|
+
)
|
128
|
+
|
129
|
+
self.jwt = self.class.generate_jwt(signin.token, signin.signinable_id)
|
90
130
|
end
|
91
131
|
|
92
132
|
def signout(token, ip, user_agent, skip_restrictions: [])
|
@@ -105,7 +145,7 @@ module Signinable
|
|
105
145
|
end
|
106
146
|
|
107
147
|
def last_signin
|
108
|
-
signins.last
|
148
|
+
signins.active.last
|
109
149
|
end
|
110
150
|
end
|
111
151
|
end
|
data/lib/signinable/version.rb
CHANGED