File: remember.rb

package info (click to toggle)
ruby-rodauth 2.42.0-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 812 kB
  • sloc: ruby: 7,524; javascript: 100; makefile: 4
file content (269 lines) | stat: -rw-r--r-- 8,348 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# frozen-string-literal: true

module Rodauth
  Feature.define(:remember, :Remember) do
    notice_flash "Your remember setting has been updated"
    error_flash "There was an error updating your remember setting"
    loaded_templates %w'remember'
    view 'remember', 'Change Remember Setting'
    additional_form_tags
    button 'Change Remember Setting'
    before
    before 'load_memory'
    after
    after 'load_memory'
    redirect
    response

    auth_value_method :raw_remember_token_deadline, nil
    auth_value_method :remember_cookie_options, {}.freeze
    auth_value_method :extend_remember_deadline?, false
    auth_value_method :extend_remember_deadline_period, 3600
    auth_value_method :remember_period, {:days=>14}.freeze
    auth_value_method :remember_deadline_interval, {:days=>14}.freeze
    auth_value_method :remember_id_column, :id
    auth_value_method :remember_key_column, :key
    auth_value_method :remember_deadline_column, :deadline
    auth_value_method :remember_table, :account_remember_keys
    auth_value_method :remember_cookie_key, '_remember'
    auth_value_method :remember_param, 'remember'
    auth_value_method :remember_remember_param_value, 'remember'
    auth_value_method :remember_forget_param_value, 'forget'
    auth_value_method :remember_disable_param_value, 'disable'
    session_key :remember_deadline_extended_session_key, :remember_deadline_extended_at
    translatable_method :remember_remember_label, 'Remember Me'
    translatable_method :remember_forget_label, 'Forget Me'
    translatable_method :remember_disable_label, 'Disable Remember Me'

    auth_methods(
      :add_remember_key,
      :disable_remember_login,
      :forget_login,
      :generate_remember_key_value,
      :get_remember_key,
      :load_memory,
      :remembered_session_id,
      :logged_in_via_remember_key?,
      :remember_key_value,
      :remember_login,
      :remove_remember_key
    )

    internal_request_method :remember_setup
    internal_request_method :remember_disable
    internal_request_method :account_id_for_remember_key

    route do |r|
      require_account
      before_remember_route

      r.get do
        remember_view
      end

      r.post do
        remember = param(remember_param)
        if [remember_remember_param_value, remember_forget_param_value, remember_disable_param_value].include?(remember)
          transaction do
            before_remember
            # :nocov:
            case remember
            # :nocov:
            when remember_remember_param_value
              remember_login
            when remember_forget_param_value
              forget_login
            when remember_disable_param_value
              disable_remember_login
            end
            after_remember
          end

          remember_response
        else
          set_response_error_reason_status(:invalid_remember_param, invalid_field_error_status)
          set_error_flash remember_error_flash
          remember_view
        end
      end
    end

    def remembered_session_id
      return unless cookie = _get_remember_cookie
      id, key = cookie.split('_', 2)
      return unless id && key

      actual, deadline = active_remember_key_ds(id).get([remember_key_column, remember_deadline_column])
      return unless actual

      if hmac_secret && !(valid = timing_safe_eql?(key, compute_hmac(actual)))
        if hmac_secret_rotation? && (valid = timing_safe_eql?(key, compute_old_hmac(actual)))
          _set_remember_cookie(id, actual, deadline)
        elsif !(raw_remember_token_deadline && raw_remember_token_deadline > convert_timestamp(deadline))
          return
        end
      end

      unless valid || timing_safe_eql?(key, actual)
        return
      end

      id
    end

    def load_memory
      if logged_in?
        if extend_remember_deadline_while_logged_in?
          if account_from_session
            extend_remember_deadline
          else
            forget_login
            clear_session
          end
        end
      elsif account_from_remember_cookie
        before_load_memory
        login_session('remember')
        extend_remember_deadline if extend_remember_deadline?
        after_load_memory
      end
    end

    def remember_login
      get_remember_key
      set_remember_cookie
      set_session_value(remember_deadline_extended_session_key, Time.now.to_i) if extend_remember_deadline?
    end

    def forget_login
      opts = Hash[remember_cookie_options]
      opts[:path] = "/" unless opts.key?(:path)
      ::Rack::Utils.delete_cookie_header!(response.headers, remember_cookie_key, opts)
    end

    def get_remember_key
      unless @remember_key_value = active_remember_key_ds.get(remember_key_column)
       generate_remember_key_value
       transaction do
         remove_remember_key
         add_remember_key
       end
      end
      nil
    end

    def disable_remember_login
      remove_remember_key
    end

    def add_remember_key
      hash = {remember_id_column=>account_id, remember_key_column=>remember_key_value}
      set_deadline_value(hash, remember_deadline_column, remember_deadline_interval)

      if e = raised_uniqueness_violation{remember_key_ds.insert(hash)}
        # If inserting into the remember key table causes a violation, we can pull the 
        # existing row from the table.  If there is no invalid row, we can then reraise.
        raise e unless @remember_key_value = active_remember_key_ds.get(remember_key_column)
      end
    end

    def remove_remember_key(id=account_id)
      remember_key_ds(id).delete
    end

    def logged_in_via_remember_key?
      authenticated_by.include?('remember')
    end

    def clear_tokens(reason)
      super
      remove_remember_key
      remember_login if logged_in? && logged_in_via_remember_key?
    end

    private

    def _set_remember_cookie(account_id, remember_key_value, deadline)
      opts = Hash[remember_cookie_options]
      opts[:value] = "#{account_id}_#{convert_token_key(remember_key_value)}"
      opts[:expires] = convert_timestamp(deadline)
      opts[:path] = "/" unless opts.key?(:path)
      opts[:httponly] = true unless opts.key?(:httponly) || opts.key?(:http_only)
      opts[:secure] = true unless opts.key?(:secure) || !request.ssl?
      ::Rack::Utils.set_cookie_header!(response.headers, remember_cookie_key, opts)
    end

    def set_remember_cookie
      _set_remember_cookie(account_id, remember_key_value, active_remember_key_ds.get(remember_deadline_column))
    end

    def extend_remember_deadline_while_logged_in?
      return false unless extend_remember_deadline?

      if extended_at = session[remember_deadline_extended_session_key]
        extended_at + extend_remember_deadline_period < Time.now.to_i
      elsif logged_in_via_remember_key?
        # Handle existing sessions before the change to extend remember deadline
        # while logged in.
        true
      end
    end

    def extend_remember_deadline
      active_remember_key_ds.update(remember_deadline_column=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, remember_period))
      remember_login
    end

    def account_from_remember_cookie
      unless id = remembered_session_id
        # Only set expired cookie if there is already a cookie set.
        forget_login if _get_remember_cookie
        return
      end

      set_session_value(session_key, id)
      account_from_session
      remove_session_value(session_key)

      unless account
        remove_remember_key(id)
        forget_login
        return
      end

      account
    end

    def _get_remember_cookie
      request.cookies[remember_cookie_key]
    end

    def after_logout
      forget_login
      super if defined?(super)
    end

    def after_close_account
      remove_remember_key
      super if defined?(super)
    end

    attr_reader :remember_key_value

    def generate_remember_key_value
      @remember_key_value = random_key
    end

    def use_date_arithmetic?
      super || extend_remember_deadline? || db.database_type == :mysql
    end

    def remember_key_ds(id=account_id)
      db[remember_table].where(remember_id_column=>id)
    end

    def active_remember_key_ds(id=account_id)
      remember_key_ds(id).where(Sequel.expr(remember_deadline_column) > Sequel::CURRENT_TIMESTAMP)
    end
  end
end