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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcefec413ade50e4c25e5092ce23033646cb6ed3b7e19bbe473011ecbf4bb649
4
- data.tar.gz: e9c6374287e726ce2007b3a6654063d017138161cfaf51cb2076167e629708d2
3
+ metadata.gz: e090b91cbbcbbfe860ca07984e3a19a965cc96bf2909f4e469e5d0ed48794dbe
4
+ data.tar.gz: c84bacb35618b62991ab60c984ec8f8c9786d783c3dd3dc8dc6a9ea76ed4881e
5
5
  SHA512:
6
- metadata.gz: '07509bfee44ecffa2072e9c7730c8b9aa557c60bf1d3d599366f9b793b77b7a4934f7de800a77157bfdba2773be1e7908343ef11d90550a6d52fccb8d8982786'
7
- data.tar.gz: ecd7ec4b2e43ff9d6a219d255674a8ee0663d190549123af5998300ba1ce52d4c972b9636b433fe685fa43d9da6159303a650c9f2937453c01e40b35719da91f
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 = SecureRandom.urlsafe_base64(rand(50..100))
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
- update!(expiration_time: (Time.zone.now + period))
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
- serialize :custom_data if ActiveRecord::Base.connection.instance_values['config'][:adapter].match('mysql')
40
+ private
41
+
42
+ def generate_token
43
+ SecureRandom.urlsafe_base64(rand(50..100))
44
+ end
34
45
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'jwt'
4
+
3
5
  module Signinable
4
6
  class Engine < ::Rails::Engine
5
7
  initializer :append_migrations do |app|
@@ -6,30 +6,32 @@ module Signinable
6
6
 
7
7
  module ClassMethods
8
8
  ALLOWED_RESTRICTIONS = %i[ip user_agent].freeze
9
- DEFAULT_EXPIRATION = 7200
9
+ DEFAULT_REFRESH_EXP = 7200
10
+ DEFAULT_JWT_EXP = 900
10
11
 
11
- cattr_reader :signin_expiration
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.signin_expiration = options.fetch(:expiration, DEFAULT_EXPIRATION)
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(token, ip, user_agent, skip_restrictions: [])
24
- signin = Signin.find_by(token: token)
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
- return unless signin
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 signin_expiration.call if signin_expiration.respond_to?(:call)
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
- signin_expiration
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 :signin_expiration
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.create!(
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
- ).token
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Signinable
4
- VERSION = '2.0.12'
4
+ VERSION = '2.0.13'
5
5
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class User < ActiveRecord::Base
4
- signinable
4
+ signinable jwt_secret: 'test', jwt_exp: 100
5
5
  end