authie 3.3.1 → 4.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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