authentication-logic 0.1.0

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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/bin/console +11 -0
  3. data/bin/setup +8 -0
  4. data/lib/auth/logic/acts_as_authentic/base.rb +118 -0
  5. data/lib/auth/logic/acts_as_authentic/email.rb +32 -0
  6. data/lib/auth/logic/acts_as_authentic/logged_in_status.rb +87 -0
  7. data/lib/auth/logic/acts_as_authentic/login.rb +65 -0
  8. data/lib/auth/logic/acts_as_authentic/magic_columns.rb +40 -0
  9. data/lib/auth/logic/acts_as_authentic/password.rb +362 -0
  10. data/lib/auth/logic/acts_as_authentic/perishable_token.rb +125 -0
  11. data/lib/auth/logic/acts_as_authentic/persistence_token.rb +72 -0
  12. data/lib/auth/logic/acts_as_authentic/queries/case_sensitivity.rb +55 -0
  13. data/lib/auth/logic/acts_as_authentic/queries/find_with_case.rb +85 -0
  14. data/lib/auth/logic/acts_as_authentic/session_maintenance.rb +189 -0
  15. data/lib/auth/logic/acts_as_authentic/single_access_token.rb +85 -0
  16. data/lib/auth/logic/config.rb +41 -0
  17. data/lib/auth/logic/controller_adapters/abstract_adapter.rb +121 -0
  18. data/lib/auth/logic/controller_adapters/rack_adapter.rb +74 -0
  19. data/lib/auth/logic/controller_adapters/rails_adapter.rb +49 -0
  20. data/lib/auth/logic/controller_adapters/sinatra_adapter.rb +69 -0
  21. data/lib/auth/logic/cookie_credentials.rb +65 -0
  22. data/lib/auth/logic/crypto_providers/bcrypt.rb +116 -0
  23. data/lib/auth/logic/crypto_providers/md5/v2.rb +37 -0
  24. data/lib/auth/logic/crypto_providers/md5.rb +38 -0
  25. data/lib/auth/logic/crypto_providers/scrypt.rb +96 -0
  26. data/lib/auth/logic/crypto_providers/sha1/v2.rb +42 -0
  27. data/lib/auth/logic/crypto_providers/sha1.rb +43 -0
  28. data/lib/auth/logic/crypto_providers/sha256/v2.rb +60 -0
  29. data/lib/auth/logic/crypto_providers/sha256.rb +61 -0
  30. data/lib/auth/logic/crypto_providers/sha512/v2.rb +41 -0
  31. data/lib/auth/logic/crypto_providers/sha512.rb +40 -0
  32. data/lib/auth/logic/crypto_providers.rb +89 -0
  33. data/lib/auth/logic/errors.rb +52 -0
  34. data/lib/auth/logic/i18n/translator.rb +20 -0
  35. data/lib/auth/logic/i18n.rb +100 -0
  36. data/lib/auth/logic/random.rb +18 -0
  37. data/lib/auth/logic/session/base.rb +2205 -0
  38. data/lib/auth/logic/session/magic_column/assigns_last_request_at.rb +49 -0
  39. data/lib/auth/logic/test_case/mock_api_controller.rb +53 -0
  40. data/lib/auth/logic/test_case/mock_controller.rb +59 -0
  41. data/lib/auth/logic/test_case/mock_cookie_jar.rb +112 -0
  42. data/lib/auth/logic/test_case/mock_logger.rb +14 -0
  43. data/lib/auth/logic/test_case/mock_request.rb +36 -0
  44. data/lib/auth/logic/test_case/rails_request_adapter.rb +40 -0
  45. data/lib/auth/logic/test_case.rb +216 -0
  46. data/lib/auth/logic/version.rb +7 -0
  47. data/lib/auth/logic.rb +46 -0
  48. metadata +426 -0
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authentication
4
+ module Logic
5
+ module ActsAsAuthentic
6
+ # This is one of my favorite features that I think is pretty cool. It's
7
+ # things like this that make a library great and let you know you are on the
8
+ # right track.
9
+ #
10
+ # Just to clear up any confusion, Authentication::Logic stores both the record id and
11
+ # the persistence token in the session. Why? So stale sessions can not be
12
+ # persisted. It stores the id so it can quickly find the record, and the
13
+ # persistence token to ensure no sessions are stale. So if the persistence
14
+ # token changes, the user must log back in.
15
+ #
16
+ # Well, the persistence token changes with the password. What happens if the
17
+ # user changes his own password? He shouldn't have to log back in, he's the
18
+ # one that made the change.
19
+ #
20
+ # That being said, wouldn't it be nice if their session and cookie
21
+ # information was automatically updated? Instead of cluttering up your
22
+ # controller with redundant session code. The same thing goes for new
23
+ # registrations.
24
+ #
25
+ # That's what this module is all about. This will automatically maintain the
26
+ # cookie and session values as records are saved.
27
+ module SessionMaintenance
28
+ def self.included(klass)
29
+ klass.class_eval do
30
+ extend Config
31
+ add_acts_as_authentic_module(Methods)
32
+ end
33
+ end
34
+
35
+ # Configuration for the session maintenance aspect of acts_as_authentic.
36
+ # These methods become class methods of ::ActiveRecord::Base.
37
+ module Config
38
+ # In order to turn off automatic maintenance of sessions
39
+ # after create, just set this to false.
40
+ #
41
+ # * <tt>Default:</tt> true
42
+ # * <tt>Accepts:</tt> Boolean
43
+ def log_in_after_create(value = nil)
44
+ rw_config(:log_in_after_create, value, true)
45
+ end
46
+ alias log_in_after_create= log_in_after_create
47
+
48
+ # In order to turn off automatic maintenance of sessions when updating
49
+ # the password, just set this to false.
50
+ #
51
+ # * <tt>Default:</tt> true
52
+ # * <tt>Accepts:</tt> Boolean
53
+ def log_in_after_password_change(value = nil)
54
+ rw_config(:log_in_after_password_change, value, true)
55
+ end
56
+ alias log_in_after_password_change= log_in_after_password_change
57
+
58
+ # As you may know, auth-logic sessions can be separate by id (See
59
+ # Authentication::Logic::Session::Base#id). You can specify here what session ids
60
+ # you want auto maintained. By default it is the main session, which has
61
+ # an id of nil.
62
+ #
63
+ # * <tt>Default:</tt> [nil]
64
+ # * <tt>Accepts:</tt> Array
65
+ def session_ids(value = nil)
66
+ rw_config(:session_ids, value, [nil])
67
+ end
68
+ alias session_ids= session_ids
69
+
70
+ # The name of the associated session class. This is inferred by the name
71
+ # of the model.
72
+ #
73
+ # * <tt>Default:</tt> "#{klass.name}Session".constantize
74
+ # * <tt>Accepts:</tt> Class
75
+ def session_class(value = nil)
76
+ const = begin
77
+ "#{base_class.name}Session".constantize
78
+ rescue NameError
79
+ nil
80
+ end
81
+ rw_config(:session_class, value, const)
82
+ end
83
+ alias session_class= session_class
84
+ end
85
+
86
+ # This module, as one of the `acts_as_authentic_modules`, is only included
87
+ # into an ActiveRecord model if that model calls `acts_as_authentic`.
88
+ module Methods
89
+ def self.included(klass)
90
+ klass.class_eval do
91
+ before_save :get_session_information, if: :update_sessions?
92
+ before_save :maintain_sessions, if: :update_sessions?
93
+ end
94
+ end
95
+
96
+ # Save the record and skip session maintenance all together.
97
+ def save_without_session_maintenance(**options)
98
+ self.skip_session_maintenance = true
99
+ result = save(**options)
100
+ self.skip_session_maintenance = false
101
+ result
102
+ end
103
+
104
+ private
105
+
106
+ def skip_session_maintenance=(value)
107
+ @skip_session_maintenance = value
108
+ end
109
+
110
+ def skip_session_maintenance
111
+ @skip_session_maintenance ||= false
112
+ end
113
+
114
+ def update_sessions?
115
+ !skip_session_maintenance &&
116
+ session_class &&
117
+ session_class.activated? &&
118
+ maintain_session? &&
119
+ !session_ids.blank? &&
120
+ will_save_change_to_persistence_token?
121
+ end
122
+
123
+ def maintain_session?
124
+ log_in_after_create? || log_in_after_password_change?
125
+ end
126
+
127
+ def get_session_information
128
+ # Need to determine if we are completely logged out, or logged in as
129
+ # another user.
130
+ @_sessions = []
131
+
132
+ session_ids.each do |session_id|
133
+ session = session_class.find(session_id, self)
134
+ @_sessions << session if session&.record
135
+ end
136
+ end
137
+
138
+ def maintain_sessions
139
+ if @_sessions.empty?
140
+ create_session
141
+ else
142
+ update_sessions
143
+ end
144
+ end
145
+
146
+ def create_session
147
+ # We only want to automatically login into the first session, since
148
+ # this is the main session. The other sessions are sessions that
149
+ # need to be created after logging into the main session.
150
+ session_id = session_ids.first
151
+ session_class.create(*[self, self, session_id].compact)
152
+
153
+ true
154
+ end
155
+
156
+ def update_sessions
157
+ # We found sessions above, let's update them with the new info
158
+ @_sessions.each do |stale_session|
159
+ next if stale_session.record != self
160
+
161
+ stale_session.unauthorized_record = self
162
+ stale_session.save
163
+ end
164
+
165
+ true
166
+ end
167
+
168
+ def session_ids
169
+ self.class.session_ids
170
+ end
171
+
172
+ def session_class
173
+ self.class.session_class
174
+ end
175
+
176
+ def log_in_after_create?
177
+ new_record? && self.class.log_in_after_create
178
+ end
179
+
180
+ def log_in_after_password_change?
181
+ persisted? &&
182
+ will_save_change_to_persistence_token? &&
183
+ self.class.log_in_after_password_change
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authentication
4
+ module Logic
5
+ module ActsAsAuthentic
6
+ # This module is responsible for maintaining the single_access token. For
7
+ # more information the single access token and how to use it, see "Params"
8
+ # in `Session::Base`.
9
+ module SingleAccessToken
10
+ def self.included(klass)
11
+ klass.class_eval do
12
+ extend Config
13
+ add_acts_as_authentic_module(Methods)
14
+ end
15
+ end
16
+
17
+ # All configuration for the single_access token aspect of acts_as_authentic.
18
+ #
19
+ # These methods become class methods of ::ActiveRecord::Base.
20
+ module Config
21
+ # The single access token is used for authentication via URLs, such as a private
22
+ # feed. That being said, if the user changes their password, that token probably
23
+ # shouldn't change. If it did, the user would have to update all of their URLs. So
24
+ # be default this is option is disabled, if you need it, feel free to turn it on.
25
+ #
26
+ # * <tt>Default:</tt> false
27
+ # * <tt>Accepts:</tt> Boolean
28
+ def change_single_access_token_with_password(value = nil)
29
+ rw_config(:change_single_access_token_with_password, value, false)
30
+ end
31
+ alias change_single_access_token_with_password= change_single_access_token_with_password
32
+ end
33
+
34
+ # All method, for the single_access token aspect of acts_as_authentic.
35
+ #
36
+ # This module, as one of the `acts_as_authentic_modules`, is only included
37
+ # into an ActiveRecord model if that model calls `acts_as_authentic`.
38
+ module Methods
39
+ def self.included(klass)
40
+ return unless klass.column_names.include?("single_access_token")
41
+
42
+ klass.class_eval do
43
+ include InstanceMethods
44
+ validates_uniqueness_of :single_access_token,
45
+ case_sensitive: true,
46
+ if: :will_save_change_to_single_access_token?
47
+
48
+ before_validation :reset_single_access_token, if: :reset_single_access_token?
49
+ if respond_to?(:after_password_set)
50
+ after_password_set(
51
+ :reset_single_access_token,
52
+ if: :change_single_access_token_with_password?
53
+ )
54
+ end
55
+ end
56
+ end
57
+
58
+ # :nodoc:
59
+ module InstanceMethods
60
+ # Resets the single_access_token to a random friendly token.
61
+ def reset_single_access_token
62
+ self.single_access_token = Authentication::Logic::Random.friendly_token
63
+ end
64
+
65
+ # same as reset_single_access_token, but then saves the record.
66
+ def reset_single_access_token!
67
+ reset_single_access_token
68
+ save_without_session_maintenance
69
+ end
70
+
71
+ protected
72
+
73
+ def reset_single_access_token?
74
+ single_access_token.blank?
75
+ end
76
+
77
+ def change_single_access_token_with_password?
78
+ self.class.change_single_access_token_with_password == true
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authentication
4
+ module Logic
5
+ # Mixed into `Authentication::Logic::ActsAsAuthentic::Base` and
6
+ # `Authentication::Logic::Session::Base`.
7
+ module Config
8
+ E_USE_NORMAL_RAILS_VALIDATION = <<~EOS
9
+ This Authentication::Logic configuration option (%s) is deprecated. Use normal
10
+ ActiveRecord validation instead. Detailed instructions:
11
+ https://github.com/vinccool96/auth-logic/blob/master/doc/use_normal_rails_validation.md
12
+ EOS
13
+
14
+ def self.extended(klass)
15
+ klass.class_eval do
16
+ class_attribute :acts_as_authentic_config
17
+ self.acts_as_authentic_config ||= {}
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def deprecate_auth_logic_config(method_name)
24
+ ::ActiveSupport::Deprecation.warn(
25
+ format(E_USE_NORMAL_RAILS_VALIDATION, method_name)
26
+ )
27
+ end
28
+
29
+ # This is a one-liner method to write a config setting, read the config
30
+ # setting, and also set a default value for the setting.
31
+ def rw_config(key, value, default_value = nil)
32
+ if value.nil?
33
+ acts_as_authentic_config.include?(key) ? acts_as_authentic_config[key] : default_value
34
+ else
35
+ self.acts_as_authentic_config = acts_as_authentic_config.merge(key => value)
36
+ value
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authentication
4
+ module Logic
5
+ module ControllerAdapters # :nodoc:
6
+ # Allows you to use Authentication::Logic in any framework you want, not just rails. See
7
+ # the RailsAdapter for an example of how to adapt Authentication::Logic to work with
8
+ # your framework.
9
+ class AbstractAdapter
10
+ E_COOKIE_DOMAIN_ADAPTER = "The cookie_domain method has not been " \
11
+ "implemented by the controller adapter"
12
+ ENV_SESSION_OPTIONS = "rack.session.options"
13
+
14
+ attr_accessor :controller
15
+
16
+ def initialize(controller)
17
+ self.controller = controller
18
+ end
19
+
20
+ def authenticate_with_http_basic
21
+ @auth = Rack::Auth::Basic::Request.new(controller.request.env)
22
+ if @auth.provided? && @auth.basic?
23
+ yield(*@auth.credentials)
24
+ else
25
+ false
26
+ end
27
+ end
28
+
29
+ def cookies
30
+ controller.cookies
31
+ end
32
+
33
+ def cookie_domain
34
+ raise NotImplementedError, E_COOKIE_DOMAIN_ADAPTER
35
+ end
36
+
37
+ def params
38
+ controller.params
39
+ end
40
+
41
+ def request
42
+ controller.request
43
+ end
44
+
45
+ def request_content_type
46
+ request.content_type
47
+ end
48
+
49
+ # Inform Rack that we would like a new session ID to be assigned. Changes
50
+ # the ID, but not the contents of the session.
51
+ #
52
+ # The `:renew` option is read by `rack/session/abstract/id.rb`.
53
+ #
54
+ # This is how Devise (via warden) implements defense against Session
55
+ # Fixation. Our implementation is copied directly from the warden gem
56
+ # (set_user in warden/proxy.rb)
57
+ def renew_session_id
58
+ env = request.env
59
+ options = env[ENV_SESSION_OPTIONS]
60
+ return unless options
61
+
62
+ if options.frozen?
63
+ env[ENV_SESSION_OPTIONS] = options.merge(renew: true).freeze
64
+ else
65
+ options[:renew] = true
66
+ end
67
+ end
68
+
69
+ def session
70
+ controller.session
71
+ end
72
+
73
+ def responds_to_single_access_allowed?
74
+ controller.respond_to?(:single_access_allowed?, true)
75
+ end
76
+
77
+ def single_access_allowed?
78
+ controller.send(:single_access_allowed?)
79
+ end
80
+
81
+ # You can disable the updating of `last_request_at`
82
+ # on a per-controller basis.
83
+ #
84
+ # # in your controller
85
+ # def last_request_update_allowed?
86
+ # false
87
+ # end
88
+ #
89
+ # For example, what if you had a javascript function that polled the
90
+ # server updating how much time is left in their session before it
91
+ # times out. Obviously you would want to ignore this request, because
92
+ # then the user would never time out. So you can do something like
93
+ # this in your controller:
94
+ #
95
+ # def last_request_update_allowed?
96
+ # action_name != "update_session_time_left"
97
+ # end
98
+ #
99
+ # See `auth/logic/session/magic_columns.rb` to learn more about the
100
+ # `last_request_at` column itself.
101
+ def last_request_update_allowed?
102
+ if controller.respond_to?(:last_request_update_allowed?, true)
103
+ controller.send(:last_request_update_allowed?)
104
+ else
105
+ true
106
+ end
107
+ end
108
+
109
+ def respond_to_missing?(*args)
110
+ super(*args) || controller.respond_to?(*args)
111
+ end
112
+
113
+ private
114
+
115
+ def method_missing(id, *args, &block)
116
+ controller.send(id, *args, &block)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authentication
4
+ module Logic
5
+ module ControllerAdapters
6
+ # Adapter for auth-logic to make it function as a Rack middleware.
7
+ # First you'll have write your own Rack adapter where you have to set your cookie domain.
8
+ #
9
+ # class YourRackAdapter < Authentication::Logic::ControllerAdapters::RackAdapter
10
+ # def cookie_domain
11
+ # 'your_cookie_domain_here.com'
12
+ # end
13
+ # end
14
+ #
15
+ # Next you need to set up a rack middleware like this:
16
+ #
17
+ # class Authentication::LogicMiddleware
18
+ # def initialize(app)
19
+ # @app = app
20
+ # end
21
+ #
22
+ # def call(env)
23
+ # YourRackAdapter.new(env)
24
+ # @app.call(env)
25
+ # end
26
+ # end
27
+ #
28
+ # And that is all! Now just load this middleware into rack:
29
+ #
30
+ # use Authentication::LogicMiddleware
31
+ #
32
+ # Authentication::Logic will expect a User and a UserSession object to be present:
33
+ #
34
+ # class UserSession < Authentication::Logic::Session::Base
35
+ # # Authentication::Logic options go here
36
+ # end
37
+ #
38
+ # class User < ApplicationRecord
39
+ # acts_as_authentic
40
+ # end
41
+ #
42
+ class RackAdapter < AbstractAdapter
43
+ def initialize(env)
44
+ # We use the Rack::Request object as the controller object.
45
+ # For this to work, we have to add some glue.
46
+ request = Rack::Request.new(env)
47
+
48
+ request.instance_eval do
49
+ def request
50
+ self
51
+ end
52
+
53
+ def remote_ip
54
+ ip
55
+ end
56
+ end
57
+
58
+ super(request)
59
+ Authentication::Logic::Session::Base.controller = self
60
+ end
61
+
62
+ # Rack Requests stores cookies with not just the value, but also with
63
+ # flags and expire information in the hash. Authentication::Logic does not like this,
64
+ # so we drop everything except the cookie value.
65
+ def cookies
66
+ controller
67
+ .cookies
68
+ .map { |key, value_hash| { key => value_hash[:value] } }
69
+ .inject(:merge) || {}
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authentication
4
+ module Logic
5
+ module ControllerAdapters
6
+ # Adapts auth-logic to work with rails. The point is to close the gap between
7
+ # what auth-logic expects and what the rails controller object provides.
8
+ # Similar to how ActiveRecord has an adapter for MySQL, PostgreSQL, SQLite,
9
+ # etc.
10
+ class RailsAdapter < AbstractAdapter
11
+ def authenticate_with_http_basic(&block)
12
+ controller.authenticate_with_http_basic(&block)
13
+ end
14
+
15
+ # Returns a `ActionDispatch::Cookies::CookieJar`. See the AC guide
16
+ # http://guides.rubyonrails.org/action_controller_overview.html#cookies
17
+ def cookies
18
+ controller.respond_to?(:cookies, true) ? controller.send(:cookies) : nil
19
+ end
20
+
21
+ def cookie_domain
22
+ controller.request.session_options[:domain]
23
+ end
24
+
25
+ def request_content_type
26
+ request.format.to_s
27
+ end
28
+
29
+ # Lets Authentication::Logic know about the controller object via a before filter, AKA
30
+ # "activates" auth-logic.
31
+ module RailsImplementation
32
+ def self.included(klass) # :nodoc:
33
+ klass.prepend_before_action :activate_auth_logic
34
+ end
35
+
36
+ private
37
+
38
+ def activate_auth_logic
39
+ Authentication::Logic::Session::Base.controller = RailsAdapter.new(self)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ ActiveSupport.on_load(:action_controller) do
48
+ include Authentication::Logic::ControllerAdapters::RailsAdapter::RailsImplementation
49
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Authentication::Logic bridge for Sinatra
4
+ module Authentication
5
+ module Logic
6
+ module ControllerAdapters
7
+ module SinatraAdapter
8
+ # Cookie management functions
9
+ class Cookies
10
+ attr_reader :request, :response
11
+
12
+ def initialize(request, response)
13
+ @request = request
14
+ @response = response
15
+ end
16
+
17
+ def delete(key, options = {})
18
+ @response.delete_cookie(key, options)
19
+ end
20
+
21
+ def []=(key, options)
22
+ @response.set_cookie(key, options)
23
+ end
24
+
25
+ def method_missing(meth, *args, &block)
26
+ @request.cookies.send(meth, *args, &block)
27
+ end
28
+ end
29
+
30
+ # Thin wrapper around request and response.
31
+ class Controller
32
+ attr_reader :request, :response, :cookies
33
+
34
+ def initialize(request, response)
35
+ @request = request
36
+ @cookies = Cookies.new(request, response)
37
+ end
38
+
39
+ def session
40
+ env["rack.session"]
41
+ end
42
+
43
+ def method_missing(meth, *args, &block)
44
+ @request.send meth, *args, &block
45
+ end
46
+ end
47
+
48
+ # Sinatra controller adapter
49
+ class Adapter < AbstractAdapter
50
+ def cookie_domain
51
+ env["SERVER_NAME"]
52
+ end
53
+
54
+ # Mixed into `Sinatra::Base`
55
+ module Implementation
56
+ def self.included(klass)
57
+ klass.send :before do
58
+ controller = Controller.new(request, response)
59
+ Authentication::Logic::Session::Base.controller = Adapter.new(controller)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ Sinatra::Base.include Authentication::Logic::ControllerAdapters::SinatraAdapter::Adapter::Implementation
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authentication
4
+ module Logic
5
+ # Represents the credentials *in* the cookie. The value of the cookie.
6
+ # This is primarily a data object. It doesn't interact with controllers.
7
+ # It doesn't know about eg. cookie expiration.
8
+ #
9
+ # @api private
10
+ class CookieCredentials
11
+ # @api private
12
+ class ParseError < RuntimeError
13
+ end
14
+
15
+ DELIMITER = "::"
16
+
17
+ attr_reader :persistence_token, :record_id, :remember_me_until
18
+
19
+ # @api private
20
+ # @param persistence_token [String]
21
+ # @param record_id [String, Numeric]
22
+ # @param remember_me_until [ActiveSupport::TimeWithZone]
23
+ def initialize(persistence_token, record_id, remember_me_until)
24
+ @persistence_token = persistence_token
25
+ @record_id = record_id
26
+ @remember_me_until = remember_me_until
27
+ end
28
+
29
+ class << self
30
+ # @api private
31
+ def parse(string)
32
+ parts = string.split(DELIMITER)
33
+ raise ParseError, format("Expected 1..3 parts, got %d", parts.length) unless (1..3).cover?(parts.length)
34
+
35
+ new(parts[0], parts[1], parse_time(parts[2]))
36
+ end
37
+
38
+ private
39
+
40
+ # @api private
41
+ def parse_time(string)
42
+ return if string.nil?
43
+
44
+ ::Time.parse(string)
45
+ rescue ::ArgumentError => e
46
+ raise ParseError, format("Found cookie, cannot parse remember_me_until: #{e}")
47
+ end
48
+ end
49
+
50
+ # @api private
51
+ def remember_me?
52
+ !@remember_me_until.nil?
53
+ end
54
+
55
+ # @api private
56
+ def to_s
57
+ [
58
+ @persistence_token,
59
+ @record_id.to_s,
60
+ @remember_me_until&.iso8601
61
+ ].compact.join(DELIMITER)
62
+ end
63
+ end
64
+ end
65
+ end