authie 3.3.2 → 4.0.0.rc3

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