authie 3.3.2 → 4.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,337 +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!(value = self.temporary_token)
138
+ # rubocop:disable Naming/AccessorMethodName
139
+ def set_cookie(value = @session.temporary_token)
91
140
  cookies[:user_session] = {
92
- :value => value,
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
- # Sets the cookie for the parent session on the associated controller.
102
- def set_parent_cookie!
103
- cookies[:parent_user_session] = {
104
- :value => cookies[:user_session],
105
- :secure => controller.request.ssl?,
106
- :httponly => true,
107
- :expires => self.expires_at
108
- }
109
- Authie.config.events.dispatch(:parent_session_cookie_updated, self)
110
- true
151
+ def cookies
152
+ @controller.send(:cookies)
111
153
  end
112
154
 
113
- # Check the security of the session to ensure it can be used.
114
- def check_security!
115
- if controller
116
- if cookies[:browser_id] != self.browser_id
117
- invalidate!
118
- Authie.config.events.dispatch(:browser_id_mismatch_error, self)
119
- raise BrowserMismatch, "Browser ID mismatch"
120
- end
121
-
122
- unless self.active?
123
- invalidate!
124
- Authie.config.events.dispatch(:invalid_session_error, self)
125
- raise InactiveSession, "Session is no longer active"
126
- end
127
-
128
- if self.expired?
129
- invalidate!
130
- Authie.config.events.dispatch(:expired_session_error, self)
131
- raise ExpiredSession, "Persistent session has expired"
132
- end
133
-
134
- if self.inactive?
135
- invalidate!
136
- Authie.config.events.dispatch(:inactive_session_error, self)
137
- raise InactiveSession, "Non-persistent session has expired"
138
- end
139
-
140
- if self.host && self.host != controller.request.host
141
- invalidate!
142
- Authie.config.events.dispatch(:host_mismatch_error, self)
143
- raise HostMismatch, "Session was created on #{self.host} but accessed using #{controller.request.host}"
144
- end
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'
145
160
  end
146
- end
147
-
148
- # Has this persistent session expired?
149
- def expired?
150
- self.expires_at &&
151
- self.expires_at < Time.now
152
- end
153
-
154
- # Has a non-persistent session become inactive?
155
- def inactive?
156
- self.expires_at.nil? &&
157
- self.last_activity_at &&
158
- self.last_activity_at < Authie.config.session_inactivity_timeout.ago
159
- end
160
-
161
- # Allow this session to persist rather than expiring at the end of the
162
- # current browser session
163
- def persist!
164
- self.expires_at = Authie.config.persistent_session_length.from_now
165
- self.save!
166
- set_cookie!
167
- end
168
161
 
169
- # Is this a persistent session?
170
- def persistent?
171
- !!expires_at
162
+ self
172
163
  end
173
164
 
174
- # Activate an old session
175
- def activate!
176
- self.active = true
177
- self.save!
178
- end
179
-
180
- # Mark this session as invalid
181
- def invalidate!
182
- self.active = false
183
- self.save!
184
- if controller
185
- cookies.delete(:user_session)
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'
186
170
  end
187
- Authie.config.events.dispatch(:session_invalidated, self)
188
- true
189
- end
190
-
191
- # Set some additional data in this session
192
- def set(key, value)
193
- self.data ||= {}
194
- self.data[key.to_s] = value
195
- self.save!
196
- end
197
171
 
198
- # Get some additional data from this session
199
- def get(key)
200
- (self.data ||= {})[key.to_s]
172
+ self
201
173
  end
202
174
 
203
- # Invalidate all sessions but this one for this user
204
- def invalidate_others!
205
- self.class.where("id != ?", self.id).for_user(self.user).each do |s|
206
- s.invalidate!
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'
207
180
  end
208
- end
209
181
 
210
- # Note that we have just seen the user enter their password.
211
- def see_password!
212
- self.password_seen_at = Time.now
213
- self.save!
214
- Authie.config.events.dispatch(:seen_password, self)
215
- true
182
+ self
216
183
  end
217
184
 
218
- # Have we seen the user's password recently in this sesion?
219
- def recently_seen_password?
220
- !!(self.password_seen_at && self.password_seen_at >= Authie.config.sudo_session_timeout.ago)
221
- end
222
-
223
- # Is two factor authentication required for this request?
224
- def two_factored?
225
- !!(two_factored_at || self.parent_id)
226
- end
227
-
228
- # Mark this request as two factor authoritsed
229
- def mark_as_two_factored!
230
- self.two_factored_at = Time.now
231
- self.two_factored_ip = controller.request.ip
232
- self.save!
233
- Authie.config.events.dispatch(:marked_as_two_factored, self)
234
- true
235
- end
236
-
237
- # Create a new session for impersonating for the given user
238
- def impersonate!(user)
239
- set_parent_cookie!
240
- self.class.start(controller, :user => user, :parent => self)
241
- end
242
-
243
- # Revert back to the parent session
244
- def revert_to_parent!
245
- if self.parent && cookies[:parent_user_session]
246
- self.invalidate!
247
- self.parent.activate!
248
- self.parent.controller = self.controller
249
- self.parent.set_cookie!(cookies[:parent_user_session])
250
- cookies.delete(:parent_user_session)
251
- self.parent
252
- else
253
- 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'
254
190
  end
255
- end
256
191
 
257
- # Is this the first session for this session's browser?
258
- def first_session_for_browser?
259
- self.class.where("id < ?", self.id).for_user(self.user).where(:browser_id => self.browser_id).empty?
192
+ self
260
193
  end
261
194
 
262
- # Is this the first session for the IP?
263
- def first_session_for_ip?
264
- self.class.where("id < ?", self.id).for_user(self.user).where(:login_ip => self.login_ip).empty?
265
- end
266
-
267
- # Find a session from the database for the given controller instance.
268
- # Returns a session object or :none if no session is found.
269
- def self.get_session(controller)
270
- cookies = controller.send(:cookies)
271
- if cookies[:user_session] && session = self.find_session_by_token(cookies[:user_session])
272
- session.temporary_token = cookies[:user_session]
273
- session.controller = controller
274
- session
275
- else
276
- :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}"
277
200
  end
278
- end
279
201
 
280
- # Find a session by a token (either from a hash or from the raw token)
281
- def self.find_session_by_token(token)
282
- return nil if token.blank?
283
- self.active.where("token = ? OR token_hash = ?", token, self.hash_token(token)).first
284
- 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
285
227
 
286
- # Create a new session and return the newly created session object.
287
- # Any other sessions for the browser will be invalidated.
288
- def self.start(controller, params = {})
289
- cookies = controller.send(:cookies)
290
- self.active.where(:browser_id => cookies[:browser_id]).each(&:invalidate!)
291
- user_object = params.delete(:user)
292
-
293
- session = self.new(params)
294
- session.user = user_object
295
- session.controller = controller
296
- session.browser_id = cookies[:browser_id]
297
- session.login_at = Time.now
298
- session.login_ip = controller.request.ip
299
- session.host = controller.request.host
300
- session.save!
301
- Authie.config.events.dispatch(:start_session, session)
302
- session
303
- 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?
304
236
 
305
- # Cleanup any old sessions.
306
- def self.cleanup
307
- Authie.config.events.dispatch(:before_cleanup)
308
- # Invalidate transient sessions that haven't been used
309
- self.active.where("expires_at IS NULL AND last_activity_at < ?", Authie.config.session_inactivity_timeout.ago).each(&:invalidate!)
310
- # Invalidate persistent sessions that have expired
311
- self.active.where("expires_at IS NOT NULL AND expires_at < ?", Time.now).each(&:invalidate!)
312
- Authie.config.events.dispatch(:after_cleanup)
313
- true
314
- end
237
+ session = SessionModel.find_session_by_token(cookies[:user_session])
238
+ return nil if session.blank?
315
239
 
316
- # Return a hash of a given token
317
- def self.hash_token(token)
318
- Digest::SHA256.hexdigest(token)
319
- end
320
-
321
- # Convert all existing active sessions to store their tokens in the database
322
- def self.convert_tokens_to_hashes
323
- active.where(:token_hash => nil).where("token is not null").each do |s|
324
- hash = self.hash_token(s.token)
325
- self.where(:id => s.id).update_all(:token_hash => hash, :token => nil)
240
+ session.temporary_token = cookies[:user_session]
241
+ new(controller, session)
326
242
  end
327
- end
328
-
329
- private
330
-
331
- # Return all cookies on the associated controller
332
- def cookies
333
- controller.send(:cookies)
334
- end
335
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
336
289
  end
337
290
  end