signinable 2.0.12 → 2.0.15
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 +67 -23
- data/lib/signinable/version.rb +1 -1
- data/spec/dummy/app/models/user.rb +1 -1
- data/spec/dummy/log/test.log +12230 -0
- data/spec/models/signin_spec.rb +82 -32
- data/spec/models/user_spec.rb +190 -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: 5874be18901fc2c73dc44f49b54e847a9837b3d735ba4bb5c5cb285df08ee811
|
4
|
+
data.tar.gz: b87d026e0221c8684ae4fc1576743fd2cd782fa3a145dbdc814c3ea6d332733b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 237c7de3fef5bc654629f346468beb3df87a3b707988858233d56d224c874be405ef7b4bc32d94e0ec3a48248aaeaae55fe7107d595832497a0d0fd849549d45
|
7
|
+
data.tar.gz: ec0357a307c90c515770f5de1be6457da1cb021fc0bd2dd9fba0a373cd065a32d2236807f58c2a9e68039d6bf2bd19cabb91987e6d1c1b660188cfde94fc7909
|
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,36 @@ 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(:refresh_exp, 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
|
28
|
-
return nil unless check_simultaneous_signings(signin)
|
29
|
-
return nil unless check_signin_permission(signin, { ip: ip, user_agent: user_agent }, skip_restrictions)
|
34
|
+
signinable = find_by(primary_key => jwt_payload['signinable_id'])
|
35
|
+
return nil unless signinable
|
30
36
|
|
31
|
-
|
32
|
-
|
37
|
+
signinable.jwt = jwt
|
38
|
+
signinable
|
33
39
|
end
|
34
40
|
|
35
41
|
def check_signin_permission(signin, restrictions_to_check, skip_restrictions)
|
@@ -37,16 +43,57 @@ module Signinable
|
|
37
43
|
end
|
38
44
|
|
39
45
|
def expiration_period
|
40
|
-
return
|
46
|
+
return refresh_exp.call if refresh_exp.respond_to?(:call)
|
47
|
+
|
48
|
+
refresh_exp
|
49
|
+
end
|
41
50
|
|
42
|
-
|
51
|
+
def generate_jwt(refresh_token, signinable_id)
|
52
|
+
JWT.encode(
|
53
|
+
{
|
54
|
+
refresh_token: refresh_token,
|
55
|
+
signinable_id: signinable_id,
|
56
|
+
exp: Time.zone.now.to_i + jwt_exp
|
57
|
+
},
|
58
|
+
jwt_secret,
|
59
|
+
'HS256'
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
def refresh_jwt(jwt, ip, user_agent, skip_restrictions: [])
|
64
|
+
token = refresh_token_from_jwt(jwt)
|
65
|
+
return nil unless token
|
66
|
+
|
67
|
+
signin = Signin.find_by(token: token)
|
68
|
+
|
69
|
+
return unless signin
|
70
|
+
return nil if signin.expired?
|
71
|
+
return nil unless check_signin_permission(signin, { ip: ip, user_agent: user_agent }, skip_restrictions)
|
72
|
+
|
73
|
+
signin.renew!(period: expiration_period, ip: ip, user_agent: user_agent, refresh_token: true)
|
74
|
+
signin.signinable.jwt = generate_jwt(signin.token, signin.signinable_id)
|
75
|
+
signin.signinable
|
43
76
|
end
|
44
77
|
|
45
78
|
private
|
46
79
|
|
47
|
-
cattr_writer :
|
80
|
+
cattr_writer :refresh_exp
|
48
81
|
cattr_writer :simultaneous_signings
|
49
82
|
cattr_writer :signin_restrictions
|
83
|
+
cattr_writer :jwt_secret
|
84
|
+
cattr_writer :jwt_exp
|
85
|
+
|
86
|
+
def extract_jwt_payload(jwt)
|
87
|
+
JWT.decode(jwt, jwt_secret, true, { algorithm: 'HS256' })[0]
|
88
|
+
rescue JWT::DecodeError
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def refresh_token_from_jwt(jwt)
|
93
|
+
JWT.decode(jwt, jwt_secret, true, { verify_expiration: false, algorithm: 'HS256' })[0]['refresh_token']
|
94
|
+
rescue JWT::DecodeError
|
95
|
+
nil
|
96
|
+
end
|
50
97
|
|
51
98
|
def signin_permitted?(signin, restrictions_to_check, skip_restrictions)
|
52
99
|
restriction_fields = signin_restriction_fields(signin, skip_restrictions)
|
@@ -68,25 +115,22 @@ module Signinable
|
|
68
115
|
end
|
69
116
|
(fields - skip_restrictions) & ALLOWED_RESTRICTIONS
|
70
117
|
end
|
71
|
-
|
72
|
-
def check_simultaneous_signings(signin)
|
73
|
-
return true if simultaneous_signings
|
74
|
-
|
75
|
-
signin == signin.signinable.last_signin
|
76
|
-
end
|
77
118
|
end
|
78
119
|
|
79
120
|
def signin(ip, user_agent, referer, permanent: false, custom_data: {})
|
80
121
|
expires_in = self.class.expiration_period
|
81
122
|
expiration_time = expires_in.zero? || permanent ? nil : expires_in.seconds.from_now
|
82
|
-
Signin.
|
123
|
+
Signin.where(signinable: self).active.map(&:expire!) unless self.class.simultaneous_signings
|
124
|
+
signin = Signin.create!(
|
83
125
|
signinable: self,
|
84
126
|
ip: ip,
|
85
127
|
referer: referer,
|
86
128
|
user_agent: user_agent,
|
87
129
|
expiration_time: expiration_time,
|
88
130
|
custom_data: custom_data
|
89
|
-
)
|
131
|
+
)
|
132
|
+
|
133
|
+
self.jwt = self.class.generate_jwt(signin.token, signin.signinable_id)
|
90
134
|
end
|
91
135
|
|
92
136
|
def signout(token, ip, user_agent, skip_restrictions: [])
|
@@ -105,7 +149,7 @@ module Signinable
|
|
105
149
|
end
|
106
150
|
|
107
151
|
def last_signin
|
108
|
-
signins.last
|
152
|
+
signins.active.last
|
109
153
|
end
|
110
154
|
end
|
111
155
|
end
|
data/lib/signinable/version.rb
CHANGED