authie 3.4.0 → 4.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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