signinable 2.0.12 → 2.0.15

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: 5874be18901fc2c73dc44f49b54e847a9837b3d735ba4bb5c5cb285df08ee811
4
+ data.tar.gz: b87d026e0221c8684ae4fc1576743fd2cd782fa3a145dbdc814c3ea6d332733b
5
5
  SHA512:
6
- metadata.gz: '07509bfee44ecffa2072e9c7730c8b9aa557c60bf1d3d599366f9b793b77b7a4934f7de800a77157bfdba2773be1e7908343ef11d90550a6d52fccb8d8982786'
7
- data.tar.gz: ecd7ec4b2e43ff9d6a219d255674a8ee0663d190549123af5998300ba1ce52d4c972b9636b433fe685fa43d9da6159303a650c9f2937453c01e40b35719da91f
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 = 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,36 @@ 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(: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(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)
34
+ signinable = find_by(primary_key => jwt_payload['signinable_id'])
35
+ return nil unless signinable
30
36
 
31
- signin.renew!(expiration_period) if signin.expireable?
32
- signin.signinable
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 signin_expiration.call if signin_expiration.respond_to?(:call)
46
+ return refresh_exp.call if refresh_exp.respond_to?(:call)
47
+
48
+ refresh_exp
49
+ end
41
50
 
42
- signin_expiration
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 :signin_expiration
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.create!(
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
- ).token
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
@@ -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.15'
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