authie 3.3.1 → 4.0.0.rc2

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.
@@ -1,323 +1,290 @@
1
- require 'secure_random_string'
1
+ # frozen_string_literal: true
2
+
3
+ require 'authie/session_model'
4
+ require 'authie/error'
5
+ require 'authie/config'
6
+ require 'active_support/core_ext/module/delegation'
2
7
 
3
8
  module Authie
4
- class Session < ActiveRecord::Base
9
+ class Session
10
+ # The underlying session model instance
11
+ #
12
+ # @return [Authie::SessionModel]
13
+ attr_reader :session
5
14
 
6
- # Errors which will be raised when there's an issue with a session's
7
- # validity in the request.
15
+ # A parent class that encapsulates all session validity errors.
8
16
  class ValidityError < Error; end
9
- class InactiveSession < ValidityError; end
10
- class ExpiredSession < ValidityError; end
11
- class BrowserMismatch < ValidityError; end
12
- class HostMismatch < ValidityError; end
13
-
14
- class NoParentSessionForRevert < Error; end
15
-
16
- # Set table name
17
- self.table_name = "authie_sessions"
18
-
19
- # Relationships
20
- parent_options = {:class_name => "Authie::Session"}
21
- parent_options[:optional] = true if ActiveRecord::VERSION::MAJOR >= 5
22
- belongs_to :parent, parent_options
23
-
24
- # Scopes
25
- scope :active, -> { where(:active => true) }
26
- scope :asc, -> { order(:last_activity_at => :desc) }
27
- scope :for_user, -> (user) { where(:user_type => user.class.name, :user_id => user.id) }
28
-
29
- # Attributes
30
- serialize :data, Hash
31
- attr_accessor :controller
32
- attr_accessor :temporary_token
33
17
 
34
- before_validation do
35
- if self.user_agent.is_a?(String)
36
- self.user_agent = self.user_agent[0,255]
37
- end
18
+ # Raised when a session is used but it is no longer active
19
+ class InactiveSession < ValidityError; end
38
20
 
39
- if self.last_activity_path.is_a?(String)
40
- self.last_activity_path = self.last_activity_path[0,255]
41
- end
42
- end
21
+ # Raised when a session is used but it has expired
22
+ class ExpiredSession < ValidityError; end
43
23
 
44
- before_create do
45
- self.temporary_token = SecureRandomString.new(44)
46
- self.token_hash = self.class.hash_token(self.temporary_token)
47
- if controller
48
- self.user_agent = controller.request.user_agent
49
- set_cookie!
50
- end
51
- end
24
+ # Raised when a session is used but the browser ID does not match
25
+ class BrowserMismatch < ValidityError; end
52
26
 
53
- before_destroy do
54
- cookies.delete(:user_session) if controller
55
- end
27
+ # Raised when a session is used but the hostname does not match
28
+ # the session hostname
29
+ class HostMismatch < ValidityError; end
56
30
 
57
- # Return the user that
58
- def user
59
- if self.user_id && self.user_type
60
- @user ||= self.user_type.constantize.find_by(:id => self.user_id) || :none
61
- @user == :none ? nil : @user
62
- end
31
+ # Initialize a new session object
32
+ #
33
+ # @param controller [ActionController::Base] any controller
34
+ # @param session [Authie::SessionModel] an Authie session model instance
35
+ # @return [Authie::Session]
36
+ def initialize(controller, session)
37
+ @controller = controller
38
+ @session = session
39
+ end
40
+
41
+ # Validate that the session is valid and raise and error if not
42
+ #
43
+ # @raises [Authie::Session::BrowserMismatch]
44
+ # @raises [Authie::Session::InactiveSession]
45
+ # @raises [Authie::Session::ExpiredSession]
46
+ # @raises [Authie::Session::HostMismatch]
47
+ # @return [Authie::Session]
48
+ def validate
49
+ validate_browser_id
50
+ validate_active
51
+ validate_expiry
52
+ validate_inactivity
53
+ validate_host
54
+ self
55
+ end
56
+
57
+ # Mark the current session as persistent. Will set the expiry time of the underlying
58
+ # session and update the cookie.
59
+ #
60
+ # @raises [ActiveRecord::RecordInvalid]
61
+ # @return [Authie::Session]
62
+ def persist
63
+ @session.expires_at = Authie.config.persistent_session_length.from_now
64
+ @session.save!
65
+ set_cookie
66
+ self
67
+ end
68
+
69
+ # Invalidates the current session by marking it inactive and removing the current cookie.
70
+ #
71
+ # @raises [ActiveRecord::RecordInvalid]
72
+ # @return [Authie::Session]
73
+ def invalidate
74
+ @session.invalidate!
75
+ cookies.delete(:user_session)
76
+ self
77
+ end
78
+
79
+ # Touches the current session to ensure it is currently valid and to update attributes
80
+ # which should be updatd on each request. This will raise the same errors as the #validate
81
+ # method. It will set the last activity time, IP and path as well as incrementing
82
+ # the request counter.
83
+ #
84
+ # @raises [Authie::Session::BrowserMismatch]
85
+ # @raises [Authie::Session::InactiveSession]
86
+ # @raises [Authie::Session::ExpiredSession]
87
+ # @raises [Authie::Session::HostMismatch]
88
+ # @raises [ActiveRecord::RecordInvalid]
89
+ # @return [Authie::Session]
90
+ def touch
91
+ validate
92
+ @session.last_activity_at = Time.now
93
+ @session.last_activity_ip = @controller.request.ip
94
+ @session.last_activity_path = @controller.request.path
95
+ @session.requests += 1
96
+ @session.save!
97
+ Authie.config.events.dispatch(:session_touched, self)
98
+ self
63
99
  end
64
100
 
65
- # Set the user
66
- def user=(user)
67
- if user
68
- self.user_type = user.class.name
69
- self.user_id = user.id
70
- else
71
- self.user_type = nil
72
- self.user_id = nil
73
- end
101
+ # Mark the session's password as seen at the current time
102
+ #
103
+ # @raises [ActiveRecord::RecordInvalid]
104
+ # @return [Authie::Session]
105
+ def see_password
106
+ @session.password_seen_at = Time.now
107
+ @session.save!
108
+ Authie.config.events.dispatch(:seen_password, self)
109
+ self
110
+ end
111
+
112
+ # Mark this request as two factored by setting the time and the current
113
+ # IP address.
114
+ #
115
+ # @raises [ActiveRecord::RecordInvalid]
116
+ # @return [Authie::Session]
117
+ def mark_as_two_factored
118
+ @session.two_factored_at = Time.now
119
+ @session.two_factored_ip = @controller.request.ip
120
+ @session.save!
121
+ Authie.config.events.dispatch(:marked_as_two_factor, self)
122
+ self
123
+ end
124
+
125
+ # Starts a new session by setting the cookie. This should be invoked whenever
126
+ # a new session begins. It usually does not need to be called directly as it
127
+ # will be taken care of by the class-level start method.
128
+ #
129
+ # @return [Authie::Session]
130
+ def start
131
+ set_cookie
132
+ Authie.config.events.dispatch(:start_session, session)
133
+ self
74
134
  end
75
135
 
76
- # This method should be called each time a user performs an
77
- # action while authenticated with this session.
78
- def touch!
79
- self.check_security!
80
- self.last_activity_at = Time.now
81
- self.last_activity_ip = controller.request.ip
82
- self.last_activity_path = controller.request.path
83
- self.requests += 1
84
- self.save!
85
- Authie.config.events.dispatch(:session_touched, self)
86
- true
87
- end
136
+ private
88
137
 
89
- # Sets the cookie on the associated controller.
90
- def set_cookie!
138
+ # rubocop:disable Naming/AccessorMethodName
139
+ def set_cookie(value = @session.temporary_token)
91
140
  cookies[:user_session] = {
92
- :value => self.temporary_token,
93
- :secure => controller.request.ssl?,
94
- :httponly => true,
95
- :expires => self.expires_at
141
+ value: value,
142
+ secure: @controller.request.ssl?,
143
+ httponly: true,
144
+ expires: @session.expires_at
96
145
  }
97
146
  Authie.config.events.dispatch(:session_cookie_updated, self)
98
147
  true
99
148
  end
149
+ # rubocop:enable Naming/AccessorMethodName
100
150
 
101
- # Check the security of the session to ensure it can be used.
102
- def check_security!
103
- if controller
104
- if cookies[:browser_id] != self.browser_id
105
- invalidate!
106
- Authie.config.events.dispatch(:browser_id_mismatch_error, self)
107
- raise BrowserMismatch, "Browser ID mismatch"
108
- end
109
-
110
- unless self.active?
111
- invalidate!
112
- Authie.config.events.dispatch(:invalid_session_error, self)
113
- raise InactiveSession, "Session is no longer active"
114
- end
115
-
116
- if self.expired?
117
- invalidate!
118
- Authie.config.events.dispatch(:expired_session_error, self)
119
- raise ExpiredSession, "Persistent session has expired"
120
- end
121
-
122
- if self.inactive?
123
- invalidate!
124
- Authie.config.events.dispatch(:inactive_session_error, self)
125
- raise InactiveSession, "Non-persistent session has expired"
126
- end
127
-
128
- if self.host && self.host != controller.request.host
129
- invalidate!
130
- Authie.config.events.dispatch(:host_mismatch_error, self)
131
- raise HostMismatch, "Session was created on #{self.host} but accessed using #{controller.request.host}"
132
- end
133
- end
134
- end
135
-
136
- # Has this persistent session expired?
137
- def expired?
138
- self.expires_at &&
139
- self.expires_at < Time.now
140
- end
141
-
142
- # Has a non-persistent session become inactive?
143
- def inactive?
144
- self.expires_at.nil? &&
145
- self.last_activity_at &&
146
- self.last_activity_at < Authie.config.session_inactivity_timeout.ago
147
- end
148
-
149
- # Allow this session to persist rather than expiring at the end of the
150
- # current browser session
151
- def persist!
152
- self.expires_at = Authie.config.persistent_session_length.from_now
153
- self.save!
154
- set_cookie!
155
- end
156
-
157
- # Is this a persistent session?
158
- def persistent?
159
- !!expires_at
160
- end
161
-
162
- # Activate an old session
163
- def activate!
164
- self.active = true
165
- self.save!
151
+ def cookies
152
+ @controller.send(:cookies)
166
153
  end
167
154
 
168
- # Mark this session as invalid
169
- def invalidate!
170
- self.active = false
171
- self.save!
172
- if controller
173
- cookies.delete(:user_session)
155
+ def validate_browser_id
156
+ if cookies[:browser_id] != @session.browser_id
157
+ invalidate
158
+ Authie.config.events.dispatch(:browser_id_mismatch_error, self)
159
+ raise BrowserMismatch, 'Browser ID mismatch'
174
160
  end
175
- Authie.config.events.dispatch(:session_invalidated, self)
176
- true
177
- end
178
161
 
179
- # Set some additional data in this session
180
- def set(key, value)
181
- self.data ||= {}
182
- self.data[key.to_s] = value
183
- self.save!
162
+ self
184
163
  end
185
164
 
186
- # Get some additional data from this session
187
- def get(key)
188
- (self.data ||= {})[key.to_s]
189
- end
190
-
191
- # Invalidate all sessions but this one for this user
192
- def invalidate_others!
193
- self.class.where("id != ?", self.id).for_user(self.user).each do |s|
194
- s.invalidate!
165
+ def validate_active
166
+ unless @session.active?
167
+ invalidate
168
+ Authie.config.events.dispatch(:invalid_session_error, self)
169
+ raise InactiveSession, 'Session is no longer active'
195
170
  end
196
- end
197
171
 
198
- # Note that we have just seen the user enter their password.
199
- def see_password!
200
- self.password_seen_at = Time.now
201
- self.save!
202
- Authie.config.events.dispatch(:seen_password, self)
203
- true
172
+ self
204
173
  end
205
174
 
206
- # Have we seen the user's password recently in this sesion?
207
- def recently_seen_password?
208
- !!(self.password_seen_at && self.password_seen_at >= Authie.config.sudo_session_timeout.ago)
209
- end
210
-
211
- # Is two factor authentication required for this request?
212
- def two_factored?
213
- !!(two_factored_at || self.parent_id)
214
- end
215
-
216
- # Mark this request as two factor authoritsed
217
- def mark_as_two_factored!
218
- self.two_factored_at = Time.now
219
- self.two_factored_ip = controller.request.ip
220
- self.save!
221
- Authie.config.events.dispatch(:marked_as_two_factored, self)
222
- true
223
- end
175
+ def validate_expiry
176
+ if @session.expired?
177
+ invalidate
178
+ Authie.config.events.dispatch(:expired_session_error, self)
179
+ raise ExpiredSession, 'Persistent session has expired'
180
+ end
224
181
 
225
- # Create a new session for impersonating for the given user
226
- def impersonate!(user)
227
- self.class.start(controller, :user => user, :parent => self)
182
+ self
228
183
  end
229
184
 
230
- # Revert back to the parent session
231
- def revert_to_parent!
232
- if self.parent
233
- self.invalidate!
234
- self.parent.activate!
235
- self.parent.controller = self.controller
236
- self.parent.set_cookie!
237
- self.parent
238
- else
239
- raise NoParentSessionForRevert, "Session does not have a parent therefore cannot be reverted."
185
+ def validate_inactivity
186
+ if @session.inactive?
187
+ invalidate
188
+ Authie.config.events.dispatch(:inactive_session_error, self)
189
+ raise InactiveSession, 'Non-persistent session has expired'
240
190
  end
241
- end
242
191
 
243
- # Is this the first session for this session's browser?
244
- def first_session_for_browser?
245
- self.class.where("id < ?", self.id).for_user(self.user).where(:browser_id => self.browser_id).empty?
192
+ self
246
193
  end
247
194
 
248
- # Is this the first session for the IP?
249
- def first_session_for_ip?
250
- self.class.where("id < ?", self.id).for_user(self.user).where(:login_ip => self.login_ip).empty?
251
- end
252
-
253
- # Find a session from the database for the given controller instance.
254
- # Returns a session object or :none if no session is found.
255
- def self.get_session(controller)
256
- cookies = controller.send(:cookies)
257
- if cookies[:user_session] && session = self.find_session_by_token(cookies[:user_session])
258
- session.temporary_token = cookies[:user_session]
259
- session.controller = controller
260
- session
261
- else
262
- :none
195
+ def validate_host
196
+ if @session.host && @session.host != @controller.request.host
197
+ invalidate
198
+ Authie.config.events.dispatch(:host_mismatch_error, self)
199
+ raise HostMismatch, "Session was created on #{@session.host} but accessed using #{@controller.request.host}"
263
200
  end
264
- end
265
-
266
- # Find a session by a token (either from a hash or from the raw token)
267
- def self.find_session_by_token(token)
268
- return nil if token.blank?
269
- self.active.where("token = ? OR token_hash = ?", token, self.hash_token(token)).first
270
- end
271
201
 
272
- # Create a new session and return the newly created session object.
273
- # Any other sessions for the browser will be invalidated.
274
- def self.start(controller, params = {})
275
- cookies = controller.send(:cookies)
276
- self.active.where(:browser_id => cookies[:browser_id]).each(&:invalidate!)
277
- user_object = params.delete(:user)
278
-
279
- session = self.new(params)
280
- session.user = user_object
281
- session.controller = controller
282
- session.browser_id = cookies[:browser_id]
283
- session.login_at = Time.now
284
- session.login_ip = controller.request.ip
285
- session.host = controller.request.host
286
- session.save!
287
- Authie.config.events.dispatch(:start_session, session)
288
- session
289
- end
202
+ self
203
+ end
204
+
205
+ class << self
206
+ # Create a new session within the given controller for the
207
+ #
208
+ # @param controller [ActionController::Base]
209
+ # @option params [ActiveRecord::Base] user
210
+ # @return [Authie::Session]
211
+ def start(controller, params = {})
212
+ cookies = controller.send(:cookies)
213
+ SessionModel.active.where(browser_id: cookies[:browser_id]).each(&:invalidate!)
214
+ user_object = params.delete(:user)
215
+
216
+ session = SessionModel.new(params)
217
+ session.user = user_object
218
+ session.browser_id = cookies[:browser_id]
219
+ session.login_at = Time.now
220
+ session.login_ip = controller.request.ip
221
+ session.host = controller.request.host
222
+ session.user_agent = controller.request.user_agent
223
+ session.save!
224
+
225
+ new(controller, session).start
226
+ end
290
227
 
291
- # Cleanup any old sessions.
292
- def self.cleanup
293
- Authie.config.events.dispatch(:before_cleanup)
294
- # Invalidate transient sessions that haven't been used
295
- self.active.where("expires_at IS NULL AND last_activity_at < ?", Authie.config.session_inactivity_timeout.ago).each(&:invalidate!)
296
- # Invalidate persistent sessions that have expired
297
- self.active.where("expires_at IS NOT NULL AND expires_at < ?", Time.now).each(&:invalidate!)
298
- Authie.config.events.dispatch(:after_cleanup)
299
- true
300
- end
228
+ # Lookup a session for a given controller and return the session
229
+ # object.
230
+ #
231
+ # @param controller [ActionController::Base]
232
+ # @return [Authie::Session]
233
+ def get_session(controller)
234
+ cookies = controller.send(:cookies)
235
+ return nil if cookies[:user_session].blank?
301
236
 
302
- # Return a hash of a given token
303
- def self.hash_token(token)
304
- Digest::SHA256.hexdigest(token)
305
- end
237
+ session = SessionModel.find_session_by_token(cookies[:user_session])
238
+ return nil if session.blank?
306
239
 
307
- # Convert all existing active sessions to store their tokens in the database
308
- def self.convert_tokens_to_hashes
309
- active.where(:token_hash => nil).where("token is not null").each do |s|
310
- hash = self.hash_token(s.token)
311
- self.where(:id => s.id).update_all(:token_hash => hash, :token => nil)
240
+ session.temporary_token = cookies[:user_session]
241
+ new(controller, session)
312
242
  end
313
- end
314
-
315
- private
316
-
317
- # Return all cookies on the associated controller
318
- def cookies
319
- controller.send(:cookies)
320
- end
321
243
 
244
+ delegate :hash_token, to: SessionModel
245
+ end
246
+
247
+ # Backwards compatibility with Authie < 4.0. These methods were all available on sessions
248
+ # in previous versions of Authie. They have been maintained for backwards-compatibility but
249
+ # will be removed entirely in Authie 5.0.
250
+ alias check_security! validate
251
+ alias persist! persist
252
+ alias invalidate! invalidate
253
+ alias touch! touch
254
+ alias set_cookie! set_cookie
255
+ alias see_password! see_password
256
+ alias mark_as_two_factored! mark_as_two_factored
257
+
258
+ # Delegate key methods back to the underlying session model. Previous behaviour in Authie
259
+ # exposed all methods on the session model. It is useful that these methods can be accessed
260
+ # easily from this session proxy model so these are maintained as delegated methods.
261
+ delegate :active?, to: :session
262
+ delegate :browser_id, to: :session
263
+ delegate :expired?, to: :session
264
+ delegate :first_session_for_browser?, to: :session
265
+ delegate :first_session_for_ip?, to: :session
266
+ delegate :get, to: :session
267
+ delegate :inactive?, to: :session
268
+ delegate :invalidate_others!, to: :session
269
+ delegate :last_activity_at, to: :session
270
+ delegate :last_activity_ip, to: :session
271
+ delegate :last_activity_path, to: :session
272
+ delegate :login_at, to: :session
273
+ delegate :login_ip, to: :session
274
+ delegate :password_seen_at, to: :session
275
+ delegate :persisted?, to: :session
276
+ delegate :persistent?, to: :session
277
+ delegate :recently_seen_password?, to: :session
278
+ delegate :requests, to: :session
279
+ delegate :set, to: :session
280
+ delegate :temporary_token, to: :session
281
+ delegate :token_hash, to: :session
282
+ delegate :two_factored_at, to: :session
283
+ delegate :two_factored_ip, to: :session
284
+ delegate :two_factored?, to: :session
285
+ delegate :update, to: :session
286
+ delegate :update!, to: :session
287
+ delegate :user_agent, to: :session
288
+ delegate :user, to: :session
322
289
  end
323
290
  end