authie 3.4.0 → 4.0.0.rc2

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: a3bbd79539a591d214d378e2f70f91ffb144fd6c0dfb31c36a4c0ea3d86bf1a5
4
- data.tar.gz: 2ba2f38bc671db30ccbde3a6eda511aa642d7d9191dbb2d4311c890ff720249c
3
+ metadata.gz: d5347676808e3554dde1670a91c8402ed166be59767298dfea961a9020659843
4
+ data.tar.gz: 03ee0e9cc60e7ae24a9e83c7a6ead5d72ebec109ef558898395d476014843a65
5
5
  SHA512:
6
- metadata.gz: 9b325154016b3844263b77a71c484158ee256ab1ff4a67b77f670d167beff464805ab4b8a654931205124389c612a22723133f46f5b0123b9ad7102343c96b50
7
- data.tar.gz: 9925f9aec3113b474b2a857676c163f570531fa5a5958d272a715fd448b7b5d2de8a1698ca23547023d8d01614009ca04035c11fc9d98ced2f83a1a228b2eaf9
6
+ metadata.gz: 219676dee96408c5cb85432ab4d919967415b9e575cb9681c4b4a9d210f041213af2a841a63ebdd4c88d9a8d3856f4ab306ddfd5e4947dee6e3d3654de8538b0
7
+ data.tar.gz: 7dd321dd2d407fd73b89ea6e1894d60362ac7e4fd792d0d74ea9e2cc5178b30eebd8d42ab3bf7baa356bc3d6f161d6809e0c545d9430d6db3267e8dfbda87190
data/lib/authie/config.rb CHANGED
@@ -4,42 +4,29 @@ require 'authie/event_manager'
4
4
 
5
5
  module Authie
6
6
  class Config
7
- def initialize
8
- @callbacks = {}
9
- end
10
-
11
- def session_inactivity_timeout
12
- @session_inactivity_timeout || 12.hours
13
- end
14
- attr_writer :session_inactivity_timeout, :persistent_session_length, :sudo_session_timeout, :browser_id_cookie_name
15
-
16
- def persistent_session_length
17
- @persistent_session_length || 2.months
18
- end
19
-
20
- def sudo_session_timeout
21
- @sudo_session_timeout || 10.minutes
22
- end
7
+ attr_accessor :session_inactivity_timeout
8
+ attr_accessor :persistent_session_length
9
+ attr_accessor :sudo_session_timeout
10
+ attr_accessor :browser_id_cookie_name
11
+ attr_accessor :events
23
12
 
24
- def user_relationship_options
25
- @user_relationship_options ||= {}
13
+ def initialize
14
+ @session_inactivity_timeout = 12.hours
15
+ @persistent_session_length = 2.months
16
+ @sudo_session_timeout = 10.minutes
17
+ @browser_id_cookie_name = :browser_id
18
+ @events = EventManager.new
26
19
  end
20
+ end
27
21
 
28
- def browser_id_cookie_name
29
- @browser_id_cookie_name || :browser_id
22
+ class << self
23
+ def config
24
+ @config ||= Config.new
30
25
  end
31
26
 
32
- def events
33
- @events ||= EventManager.new
27
+ def configure(&block)
28
+ block.call(config)
29
+ config
34
30
  end
35
31
  end
36
-
37
- def self.config
38
- @config ||= Config.new
39
- end
40
-
41
- def self.configure(&block)
42
- block.call(config)
43
- config
44
- end
45
32
  end
@@ -2,18 +2,28 @@
2
2
 
3
3
  require 'securerandom'
4
4
  require 'authie/session'
5
+ require 'authie/config'
6
+ require 'authie/session_model'
5
7
 
6
8
  module Authie
9
+ # The controller delegate implements methods that can be used by a controller. These are then
10
+ # extended into controllers as needed (see ControllerExtension).
7
11
  class ControllerDelegate
12
+ # @param controller [ActionController::Base]
13
+ # @return [Authie::ControllerDelegate]
8
14
  def initialize(controller)
9
15
  @controller = controller
10
16
  end
11
17
 
12
- # Set a random browser ID for this browser.
18
+ # Sets a browser ID. This must be performed on any page request where AUthie will be used.
19
+ # It should be triggered before any other Authie provided methods. This will ensure that
20
+ # the given browser ID is unique.
21
+ #
22
+ # @return [String] the generated browser ID
13
23
  def set_browser_id
14
24
  until cookies[Authie.config.browser_id_cookie_name]
15
25
  proposed_browser_id = SecureRandom.uuid
16
- next if Authie::Session.where(browser_id: proposed_browser_id).exists?
26
+ next if Authie::SessionModel.where(browser_id: proposed_browser_id).exists?
17
27
 
18
28
  cookies[Authie.config.browser_id_cookie_name] = {
19
29
  value: proposed_browser_id,
@@ -21,61 +31,81 @@ module Authie
21
31
  httponly: true,
22
32
  secure: @controller.request.ssl?
23
33
  }
24
- # Dispatch an event when the browser ID is set.
25
34
  Authie.config.events.dispatch(:set_browser_id, proposed_browser_id)
26
35
  end
36
+ proposed_browser_id
27
37
  end
28
38
 
29
- # Touch the auth session on each request if logged in
39
+ # Touch the session on each request to ensure that it is validated and all last activity
40
+ # information is updated. This will return the session if one has been touched otherwise
41
+ # it will reteurn false if there is no session/not logged in. It is safe to run this on
42
+ # all requests even if there is no session.
43
+ #
44
+ # @return [Authie::Session, false]
30
45
  def touch_auth_session
31
- auth_session.touch! if logged_in?
46
+ return auth_session.touch if logged_in?
47
+
48
+ false
32
49
  end
33
50
 
34
- # Return the currently logged in user object
51
+ # Return the user for the currently logged in user or nil if no user is logged in
52
+ #
53
+ # @return [ActiveRecord::Base, nil]
35
54
  def current_user
36
- logged_in? ? auth_session.user : nil
37
- end
55
+ return nil unless logged_in?
38
56
 
39
- # Set the currently logged in user
40
- def current_user=(user)
41
- create_auth_session(user)
57
+ auth_session.session.user
42
58
  end
43
59
 
44
- # Create a new session for the given user
60
+ # Create a new session for the given user. If nil is provided as a user, the existing session
61
+ # will be invalidated.
62
+ #
63
+ # @return [Authie::Session, nil]
45
64
  def create_auth_session(user)
46
65
  if user
47
66
  @auth_session = Authie::Session.start(@controller, user: user)
48
- else
49
- auth_session.invalidate! if logged_in?
50
- @auth_session = :none
67
+ return @auth_session
51
68
  end
69
+
70
+ invalidate_auth_session
71
+ nil
52
72
  end
53
73
 
54
- # Invalidate an existing auth session
74
+ # Invalidate the existing auth session if one exists. Return true if a sesion has been invalidated
75
+ # otherwise return false.
76
+ #
77
+ # @return [Boolean]
55
78
  def invalidate_auth_session
56
79
  if logged_in?
57
- auth_session.invalidate!
58
- @auth_session = :none
59
- true
60
- else
61
- false
80
+ auth_session.invalidate
81
+ @auth_session = nil
82
+ return true
62
83
  end
84
+
85
+ false
63
86
  end
64
87
 
65
- # Is anyone currently logged in?
88
+ # Is anyone currently logged in? Return true if there is an auth session present.
89
+ #
90
+ # Note: this does not check the validatity of the session. You must always ensure that the `validate`
91
+ # or `touch` method is invoked to ensure that the session that has been found is active.
92
+ #
93
+ # @return [Boolean]
66
94
  def logged_in?
67
95
  auth_session.is_a?(Session)
68
96
  end
69
97
 
70
- # Return the currently logged in user session
98
+ # Return an auth session that has been found in the current cookies.
99
+ #
100
+ # @return [Authie::Session]
71
101
  def auth_session
72
- @auth_session ||= Authie::Session.get_session(@controller)
73
- @auth_session == :none ? nil : @auth_session
102
+ return @auth_session if instance_variable_defined?('@auth_session')
103
+
104
+ @auth_session = Authie::Session.get_session(@controller)
74
105
  end
75
106
 
76
107
  private
77
108
 
78
- # Return cookies for the controller
79
109
  def cookies
80
110
  @controller.send(:cookies)
81
111
  end
@@ -4,10 +4,19 @@ require 'authie/controller_delegate'
4
4
 
5
5
  module Authie
6
6
  module ControllerExtension
7
- def self.included(base)
8
- base.helper_method :logged_in?, :current_user, :auth_session
9
- before_action_method = base.respond_to?(:before_action) ? :before_action : :before_filter
10
- base.public_send(before_action_method, :set_browser_id, :touch_auth_session)
7
+ class << self
8
+ def included(base)
9
+ base.helper_method :logged_in?, :current_user, :auth_session
10
+ base.before_action :set_browser_id, :touch_auth_session
11
+
12
+ base.delegate :set_browser_id, to: :auth_session_delegate
13
+ base.delegate :touch_auth_session, to: :auth_session_delegate
14
+ base.delegate :current_user, to: :auth_session_delegate
15
+ base.delegate :create_auth_session, to: :auth_session_delegate
16
+ base.delegate :invalidate_auth_session, to: :auth_session_delegate
17
+ base.delegate :logged_in?, to: :auth_session_delegate
18
+ base.delegate :auth_session, to: :auth_session_delegate
19
+ end
11
20
  end
12
21
 
13
22
  private
@@ -15,37 +24,5 @@ module Authie
15
24
  def auth_session_delegate
16
25
  @auth_session_delegate ||= Authie::ControllerDelegate.new(self)
17
26
  end
18
-
19
- def set_browser_id
20
- auth_session_delegate.set_browser_id
21
- end
22
-
23
- def touch_auth_session
24
- auth_session_delegate.touch_auth_session
25
- end
26
-
27
- def current_user
28
- auth_session_delegate.current_user
29
- end
30
-
31
- def current_user=(user)
32
- auth_session_delegate.current_user = user
33
- end
34
-
35
- def create_auth_session(user)
36
- auth_session_delegate.create_auth_session(user)
37
- end
38
-
39
- def invalidate_auth_session
40
- auth_session_delegate.invalidate_auth_session
41
- end
42
-
43
- def logged_in?
44
- auth_session_delegate.logged_in?
45
- end
46
-
47
- def auth_session
48
- auth_session_delegate.auth_session
49
- end
50
27
  end
51
28
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Authie
4
4
  class EventManager
5
+ attr_reader :callbacks
6
+
5
7
  def initialize
6
8
  @callbacks = {}
7
9
  end
@@ -1,338 +1,290 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'secure_random_string'
3
+ require 'authie/session_model'
4
+ require 'authie/error'
5
+ require 'authie/config'
6
+ require 'active_support/core_ext/module/delegation'
4
7
 
5
8
  module Authie
6
- class Session < ActiveRecord::Base
7
- # Errors which will be raised when there's an issue with a session's
8
- # validity in the request.
9
+ class Session
10
+ # The underlying session model instance
11
+ #
12
+ # @return [Authie::SessionModel]
13
+ attr_reader :session
14
+
15
+ # A parent class that encapsulates all session validity errors.
9
16
  class ValidityError < Error; end
10
17
 
18
+ # Raised when a session is used but it is no longer active
11
19
  class InactiveSession < ValidityError; end
12
20
 
21
+ # Raised when a session is used but it has expired
13
22
  class ExpiredSession < ValidityError; end
14
23
 
24
+ # Raised when a session is used but the browser ID does not match
15
25
  class BrowserMismatch < ValidityError; end
16
26
 
27
+ # Raised when a session is used but the hostname does not match
28
+ # the session hostname
17
29
  class HostMismatch < ValidityError; end
18
30
 
19
- class NoParentSessionForRevert < Error; end
20
-
21
- # Set table name
22
- self.table_name = 'authie_sessions'
23
-
24
- # Relationships
25
- parent_options = { class_name: 'Authie::Session' }
26
- parent_options[:optional] = true if ActiveRecord::VERSION::MAJOR >= 5
27
- belongs_to :parent, **parent_options
28
-
29
- # Scopes
30
- scope :active, -> { where(active: true) }
31
- scope :asc, -> { order(last_activity_at: :desc) }
32
- scope :for_user, ->(user) { where(user_type: user.class.name, user_id: user.id) }
33
-
34
- # Attributes
35
- serialize :data, Hash
36
- attr_accessor :controller, :temporary_token
37
-
38
- before_validation do
39
- self.user_agent = user_agent[0, 255] if user_agent.is_a?(String)
40
-
41
- self.last_activity_path = last_activity_path[0, 255] if last_activity_path.is_a?(String)
42
- end
43
-
44
- before_create do
45
- self.temporary_token = SecureRandomString.new(44)
46
- self.token_hash = self.class.hash_token(temporary_token)
47
- if controller
48
- self.user_agent = controller.request.user_agent
49
- set_cookie!
50
- end
51
- end
52
-
53
- before_destroy do
54
- cookies.delete(:user_session) if controller
55
- end
56
-
57
- # Return the user that
58
- def user
59
- return unless user_id && user_type
60
-
61
- @user ||= user_type.constantize.find_by(id: user_id) || :none
62
- @user == :none ? nil : @user
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
- 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
- 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
138
  # rubocop:disable Naming/AccessorMethodName
91
- def set_cookie!(value = temporary_token)
139
+ def set_cookie(value = @session.temporary_token)
92
140
  cookies[:user_session] = {
93
141
  value: value,
94
- secure: controller.request.ssl?,
142
+ secure: @controller.request.ssl?,
95
143
  httponly: true,
96
- expires: expires_at
144
+ expires: @session.expires_at
97
145
  }
98
146
  Authie.config.events.dispatch(:session_cookie_updated, self)
99
147
  true
100
148
  end
101
149
  # rubocop:enable Naming/AccessorMethodName
102
150
 
103
- # Sets the cookie for the parent session on the associated controller.
104
- def set_parent_cookie!
105
- cookies[:parent_user_session] = {
106
- value: cookies[:user_session],
107
- secure: controller.request.ssl?,
108
- httponly: true,
109
- expires: expires_at
110
- }
111
- Authie.config.events.dispatch(:parent_session_cookie_updated, self)
112
- true
151
+ def cookies
152
+ @controller.send(:cookies)
113
153
  end
114
154
 
115
- # Check the security of the session to ensure it can be used.
116
- def check_security!
117
- raise Authie::Error, 'Cannot check security without a controller' unless controller
118
-
119
- if cookies[:browser_id] != browser_id
120
- invalidate!
155
+ def validate_browser_id
156
+ if cookies[:browser_id] != @session.browser_id
157
+ invalidate
121
158
  Authie.config.events.dispatch(:browser_id_mismatch_error, self)
122
159
  raise BrowserMismatch, 'Browser ID mismatch'
123
160
  end
124
161
 
125
- unless active?
126
- invalidate!
162
+ self
163
+ end
164
+
165
+ def validate_active
166
+ unless @session.active?
167
+ invalidate
127
168
  Authie.config.events.dispatch(:invalid_session_error, self)
128
169
  raise InactiveSession, 'Session is no longer active'
129
170
  end
130
171
 
131
- if expired?
132
- invalidate!
172
+ self
173
+ end
174
+
175
+ def validate_expiry
176
+ if @session.expired?
177
+ invalidate
133
178
  Authie.config.events.dispatch(:expired_session_error, self)
134
179
  raise ExpiredSession, 'Persistent session has expired'
135
180
  end
136
181
 
137
- if inactive?
138
- invalidate!
182
+ self
183
+ end
184
+
185
+ def validate_inactivity
186
+ if @session.inactive?
187
+ invalidate
139
188
  Authie.config.events.dispatch(:inactive_session_error, self)
140
189
  raise InactiveSession, 'Non-persistent session has expired'
141
190
  end
142
191
 
143
- if host && host != controller.request.host
144
- invalidate!
145
- Authie.config.events.dispatch(:host_mismatch_error, self)
146
- raise HostMismatch, "Session was created on #{host} but accessed using #{controller.request.host}"
147
- end
148
-
149
- true
150
- end
151
-
152
- # Has this persistent session expired?
153
- def expired?
154
- expires_at &&
155
- expires_at < Time.now
156
- end
157
-
158
- # Has a non-persistent session become inactive?
159
- def inactive?
160
- expires_at.nil? &&
161
- last_activity_at &&
162
- last_activity_at < Authie.config.session_inactivity_timeout.ago
163
- end
164
-
165
- # Allow this session to persist rather than expiring at the end of the
166
- # current browser session
167
- def persist!
168
- self.expires_at = Authie.config.persistent_session_length.from_now
169
- save!
170
- set_cookie!
171
- end
172
-
173
- # Is this a persistent session?
174
- def persistent?
175
- !!expires_at
176
- end
177
-
178
- # Activate an old session
179
- def activate!
180
- self.active = true
181
- save!
182
- end
183
-
184
- # Mark this session as invalid
185
- def invalidate!
186
- self.active = false
187
- save!
188
- cookies.delete(:user_session) if controller
189
- Authie.config.events.dispatch(:session_invalidated, self)
190
- true
191
- end
192
-
193
- # Set some additional data in this session
194
- def set(key, value)
195
- self.data ||= {}
196
- self.data[key.to_s] = value
197
- save!
198
- end
199
-
200
- # Get some additional data from this session
201
- def get(key)
202
- (self.data ||= {})[key.to_s]
203
- end
204
-
205
- # Invalidate all sessions but this one for this user
206
- def invalidate_others!
207
- self.class.where('id != ?', id).for_user(user).each(&:invalidate!)
208
- end
209
-
210
- # Note that we have just seen the user enter their password.
211
- def see_password!
212
- self.password_seen_at = Time.now
213
- save!
214
- Authie.config.events.dispatch(:seen_password, self)
215
- true
216
- end
217
-
218
- # Have we seen the user's password recently in this sesion?
219
- def recently_seen_password?
220
- !!(password_seen_at && 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 || parent_id)
192
+ self
226
193
  end
227
194
 
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
- 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
- unless parent && cookies[:parent_user_session]
246
- raise NoParentSessionForRevert, 'Session does not have a parent therefore cannot be reverted.'
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}"
247
200
  end
248
201
 
249
- invalidate!
250
- parent.activate!
251
- parent.controller = controller
252
- parent.set_cookie!(cookies[:parent_user_session])
253
- cookies.delete(:parent_user_session)
254
- parent
255
- end
256
-
257
- # Is this the first session for this session's browser?
258
- def first_session_for_browser?
259
- self.class.where('id < ?', id).for_user(user).where(browser_id: browser_id).empty?
260
- end
261
-
262
- # Is this the first session for the IP?
263
- def first_session_for_ip?
264
- self.class.where('id < ?', id).for_user(user).where(login_ip: 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 = find_session_by_token(cookies[:user_session]))
272
- session.temporary_token = cookies[:user_session]
273
- session.controller = controller
274
- session
275
- else
276
- :none
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
277
226
  end
278
- end
279
-
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
227
 
284
- active.where('token = ? OR token_hash = ?', token, hash_token(token)).first
285
- 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?
286
236
 
287
- # Create a new session and return the newly created session object.
288
- # Any other sessions for the browser will be invalidated.
289
- def self.start(controller, params = {})
290
- cookies = controller.send(:cookies)
291
- active.where(browser_id: cookies[:browser_id]).each(&:invalidate!)
292
- user_object = params.delete(:user)
293
-
294
- session = new(params)
295
- session.user = user_object
296
- session.controller = controller
297
- session.browser_id = cookies[:browser_id]
298
- session.login_at = Time.now
299
- session.login_ip = controller.request.ip
300
- session.host = controller.request.host
301
- session.save!
302
- Authie.config.events.dispatch(:start_session, session)
303
- session
304
- end
237
+ session = SessionModel.find_session_by_token(cookies[:user_session])
238
+ return nil if session.blank?
305
239
 
306
- # Cleanup any old sessions.
307
- def self.cleanup
308
- Authie.config.events.dispatch(:before_cleanup)
309
- # Invalidate transient sessions that haven't been used
310
- active.where('expires_at IS NULL AND last_activity_at < ?',
311
- Authie.config.session_inactivity_timeout.ago).each(&:invalidate!)
312
- # Invalidate persistent sessions that have expired
313
- active.where('expires_at IS NOT NULL AND expires_at < ?', Time.now).each(&:invalidate!)
314
- Authie.config.events.dispatch(:after_cleanup)
315
- true
316
- end
317
-
318
- # Return a hash of a given token
319
- def self.hash_token(token)
320
- Digest::SHA256.hexdigest(token)
321
- end
322
-
323
- # Convert all existing active sessions to store their tokens in the database
324
- def self.convert_tokens_to_hashes
325
- active.where(token_hash: nil).where('token is not null').each do |s|
326
- hash = hash_token(s.token)
327
- where(id: s.id).update_all(token_hash: hash, token: nil)
240
+ session.temporary_token = cookies[:user_session]
241
+ new(controller, session)
328
242
  end
329
- end
330
-
331
- private
332
243
 
333
- # Return all cookies on the associated controller
334
- def cookies
335
- controller.send(:cookies)
336
- end
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
337
289
  end
338
290
  end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/base'
4
+ require 'secure_random_string'
5
+ require 'authie/config'
6
+
7
+ module Authie
8
+ class SessionModel < ActiveRecord::Base
9
+ attr_accessor :temporary_token
10
+
11
+ self.table_name = 'authie_sessions'
12
+
13
+ belongs_to :parent, class_name: 'Authie::SessionModel', optional: true
14
+
15
+ scope :active, -> { where(active: true) }
16
+ scope :asc, -> { order(last_activity_at: :desc) }
17
+ scope :for_user, ->(user) { where(user_type: user.class.name, user_id: user.id) }
18
+
19
+ # Attributes
20
+ serialize :data, Hash
21
+
22
+ before_validation do
23
+ self.user_agent = user_agent[0, 255] if user_agent.is_a?(String)
24
+ self.last_activity_path = last_activity_path[0, 255] if last_activity_path.is_a?(String)
25
+ end
26
+
27
+ before_create do
28
+ self.temporary_token = SecureRandomString.new(44)
29
+ self.token_hash = self.class.hash_token(temporary_token)
30
+ end
31
+
32
+ # Return the user that
33
+ def user
34
+ return unless user_id && user_type
35
+ return @user if instance_variable_defined?('@user')
36
+
37
+ @user = user_type.constantize.find_by(id: user_id)
38
+ end
39
+
40
+ # Set the user
41
+ def user=(user)
42
+ @user = user
43
+ if user
44
+ self.user_type = user.class.name
45
+ self.user_id = user.id
46
+ else
47
+ self.user_type = nil
48
+ self.user_id = nil
49
+ end
50
+ end
51
+
52
+ def expired?
53
+ expires_at.present? &&
54
+ expires_at < Time.now
55
+ end
56
+
57
+ def inactive?
58
+ expires_at.nil? &&
59
+ last_activity_at.present? &&
60
+ last_activity_at < Authie.config.session_inactivity_timeout.ago
61
+ end
62
+
63
+ def persistent?
64
+ !!expires_at
65
+ end
66
+
67
+ def activate!
68
+ self.active = true
69
+ save!
70
+ end
71
+
72
+ def invalidate!
73
+ self.active = false
74
+ save!
75
+ true
76
+ end
77
+
78
+ def set(key, value)
79
+ self.data ||= {}
80
+ self.data[key.to_s] = value
81
+ save!
82
+ end
83
+
84
+ def get(key)
85
+ (self.data ||= {})[key.to_s]
86
+ end
87
+
88
+ def invalidate_others!
89
+ self.class.where('id != ?', id).for_user(user).each(&:invalidate!).inspect
90
+ end
91
+
92
+ # Have we seen the user's password recently in this sesion?
93
+ def recently_seen_password?
94
+ !!(password_seen_at && password_seen_at >= Authie.config.sudo_session_timeout.ago)
95
+ end
96
+
97
+ # Is two factor authentication required for this request?
98
+ def two_factored?
99
+ !!(two_factored_at || parent_id)
100
+ end
101
+
102
+ # Is this the first session for this session's browser?
103
+ def first_session_for_browser?
104
+ self.class.where('id < ?', id).for_user(user).where(browser_id: browser_id).empty?
105
+ end
106
+
107
+ # Is this the first session for the IP?
108
+ def first_session_for_ip?
109
+ self.class.where('id < ?', id).for_user(user).where(login_ip: login_ip).empty?
110
+ end
111
+
112
+ class << self
113
+ # Find a session from the database for the given controller instance.
114
+ # Returns a session object or :none if no session is found.
115
+
116
+ # Find a session by a token (either from a hash or from the raw token)
117
+ def find_session_by_token(token)
118
+ return nil if token.blank?
119
+
120
+ active.where(token_hash: hash_token(token)).first
121
+ end
122
+
123
+ # Cleanup any old sessions.
124
+ def cleanup
125
+ Authie.config.events.dispatch(:before_cleanup)
126
+ # Invalidate transient sessions that haven't been used
127
+ active.where('expires_at IS NULL AND last_activity_at < ?',
128
+ Authie.config.session_inactivity_timeout.ago).each(&:invalidate!)
129
+ # Invalidate persistent sessions that have expired
130
+ active.where('expires_at IS NOT NULL AND expires_at < ?', Time.now).each(&:invalidate!)
131
+ Authie.config.events.dispatch(:after_cleanup)
132
+ true
133
+ end
134
+
135
+ # Return a hash of a given token
136
+ def hash_token(token)
137
+ Digest::SHA256.hexdigest(token)
138
+ end
139
+ end
140
+ end
141
+ end
data/lib/authie/user.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Authie
4
4
  module User
5
5
  def self.included(base)
6
- base.has_many :user_sessions, class_name: 'Authie::Session', as: :user, dependent: :delete_all
6
+ base.has_many :user_sessions, class_name: 'Authie::SessionModel', as: :user, dependent: :delete_all
7
7
  end
8
8
  end
9
9
  end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Authie
4
- VERSION = '3.4.0'
4
+ VERSION_FILE_ROOT = File.expand_path('../../VERSION', __dir__)
5
+ VERSION = if File.file?(VERSION_FILE_ROOT)
6
+ File.read(VERSION_FILE_ROOT).strip.sub(/\Av/, '')
7
+ else
8
+ '0.0.0.dev'
9
+ end
5
10
  end
metadata CHANGED
@@ -1,15 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: authie
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 4.0.0.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Cooke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-01 00:00:00.000000000 Z
11
+ date: 2022-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
13
33
  - !ruby/object:Gem::Dependency
14
34
  name: secure_random_string
15
35
  requirement: !ruby/object:Gem::Requirement
@@ -24,6 +44,194 @@ dependencies:
24
44
  - - ">="
25
45
  - !ruby/object:Gem::Version
26
46
  version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: appraisal
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: 2.4.1
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - '='
59
+ - !ruby/object:Gem::Version
60
+ version: 2.4.1
61
+ - !ruby/object:Gem::Dependency
62
+ name: rails
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '5.0'
68
+ - - "<"
69
+ - !ruby/object:Gem::Version
70
+ version: '8.0'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '5.0'
78
+ - - "<"
79
+ - !ruby/object:Gem::Version
80
+ version: '8.0'
81
+ - !ruby/object:Gem::Dependency
82
+ name: rspec
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: rspec-core
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ - !ruby/object:Gem::Dependency
110
+ name: rspec-expectations
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ - !ruby/object:Gem::Dependency
124
+ name: rspec-mocks
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ type: :development
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ - !ruby/object:Gem::Dependency
138
+ name: rspec-rails
139
+ requirement: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ type: :development
145
+ prerelease: false
146
+ version_requirements: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ - !ruby/object:Gem::Dependency
152
+ name: rubocop
153
+ requirement: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - '='
156
+ - !ruby/object:Gem::Version
157
+ version: 1.17.0
158
+ type: :development
159
+ prerelease: false
160
+ version_requirements: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - '='
163
+ - !ruby/object:Gem::Version
164
+ version: 1.17.0
165
+ - !ruby/object:Gem::Dependency
166
+ name: simplecov
167
+ requirement: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ type: :development
173
+ prerelease: false
174
+ version_requirements: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: '0'
179
+ - !ruby/object:Gem::Dependency
180
+ name: simplecov-console
181
+ requirement: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ type: :development
187
+ prerelease: false
188
+ version_requirements: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - ">="
191
+ - !ruby/object:Gem::Version
192
+ version: '0'
193
+ - !ruby/object:Gem::Dependency
194
+ name: solargraph
195
+ requirement: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - ">="
198
+ - !ruby/object:Gem::Version
199
+ version: '0'
200
+ type: :development
201
+ prerelease: false
202
+ version_requirements: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: '0'
207
+ - !ruby/object:Gem::Dependency
208
+ name: sqlite3
209
+ requirement: !ruby/object:Gem::Requirement
210
+ requirements:
211
+ - - '='
212
+ - !ruby/object:Gem::Version
213
+ version: 1.4.2
214
+ type: :development
215
+ prerelease: false
216
+ version_requirements: !ruby/object:Gem::Requirement
217
+ requirements:
218
+ - - '='
219
+ - !ruby/object:Gem::Version
220
+ version: 1.4.2
221
+ - !ruby/object:Gem::Dependency
222
+ name: timecop
223
+ requirement: !ruby/object:Gem::Requirement
224
+ requirements:
225
+ - - ">="
226
+ - !ruby/object:Gem::Version
227
+ version: '0'
228
+ type: :development
229
+ prerelease: false
230
+ version_requirements: !ruby/object:Gem::Requirement
231
+ requirements:
232
+ - - ">="
233
+ - !ruby/object:Gem::Version
234
+ version: '0'
27
235
  description: A Rails library for storing user sessions in a backend database
28
236
  email:
29
237
  - me@adamcooke.io
@@ -47,6 +255,7 @@ files:
47
255
  - lib/authie/event_manager.rb
48
256
  - lib/authie/rack_controller.rb
49
257
  - lib/authie/session.rb
258
+ - lib/authie/session_model.rb
50
259
  - lib/authie/user.rb
51
260
  - lib/authie/version.rb
52
261
  homepage: https://github.com/adamcooke/authie
@@ -64,11 +273,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
64
273
  version: '0'
65
274
  required_rubygems_version: !ruby/object:Gem::Requirement
66
275
  requirements:
67
- - - ">="
276
+ - - ">"
68
277
  - !ruby/object:Gem::Version
69
- version: '0'
278
+ version: 1.3.1
70
279
  requirements: []
71
- rubygems_version: 3.0.3
280
+ rubygems_version: 3.3.7
72
281
  signing_key:
73
282
  specification_version: 4
74
283
  summary: A Rails library for storing user sessions in a backend database