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 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