Replies: 5 comments 14 replies
-
Remember tokens are shared between browsers, the remove/add is to handle expired keys. If the account already has a remember token, |
Beta Was this translation helpful? Give feedback.
-
Come to think of it, I think a recent change to When |
Beta Was this translation helpful? Give feedback.
-
@jeremyevans We have come again past this issue, and are working internally towards a solution. To summarise: The preferred behaviour for us, when using Now, we want to do this with as little impact as possible, and we thought that the following solution would work, and could maybe be of interest to rodauth. By default rodauth creates the remember token by combining the This affords the same safeties as using the The only hiccup we have found so far, is that the base implementation of the What do you think? Is this a good solution? Would you be interested in a port of this solution into rodauth? |
Beta Was this translation helpful? Give feedback.
-
We have internally developed a "multiple_remember" feature for rodauth. It looks something like this:
Our main motivator for this remains security. The single value shared across multiple devices is still - according to us - a security vulnerability. We imagine a situation where a user signs into browser A and B with remember functionality enabled). These browsers will share the same remember cookie. In our scenario we imagine that device A remains in daily use, but B is not. Even long after the configured deadline the remember cookie on devise B can still be used to restore a session (by a malicious actor) by modifying the expires timestamp in the browser, or directly using the value in a script. I believe that this is similar to a session fixation attack. Initially we were not going to include backwards compatibility for the old cookie, but we realised that this would sign out a bunch of our users, so that was decided to not be acceptable. So we also update the cookie when we detect an old one. This bit of code will be removed after our configured deadline. Our code, at this time, is not ready to be turned into a valid PR, as we just focused on our direct needs. (eg, we don't include views as we run API only, and only cover through integration tests). For reference, here is our implementation: # frozen-string-literal: true
module Rodauth
Feature.define(:multiple_remember, :MultipleRemember) do
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
session_key :remember_deadline_extended_session_key, :remember_deadline_extended_at
auth_value_method :multiple_remember_cookie_key, '_rmmbr'
auth_value_method :multiple_remember_id_column, :id
auth_value_method :multiple_remember_account_id_column, :account_id
auth_value_method :multiple_remember_key_column, :key
auth_value_method :multiple_remember_deadline_column, :deadline
auth_value_method :multiple_remember_table, :account_multiple_remember_keys
auth_value_method :remember_cookie_key, '_remember'
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_methods(
:add_remember_key,
: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
def remembered_session_id
return unless cookie = _get_remember_cookie
account_id, key_id, key = cookie.split('_', 3)
return unless account_id && key_id && key
actual, deadline = current_remember_key_ds(account_id, key_id).get([multiple_remember_key_column, multiple_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(account_id, key_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
account_id
end
def remembered_session_key_id
return unless cookie = _get_remember_cookie
account_id, key_id, key = cookie.split('_', 3)
return unless account_id && key_id && key
return key_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
# Remove the below code branch once all active sessions in production are updated
if update_remember_token_to_key_based_remember_token?
update_remember_cookie
# Delete the old cookie, no matter what
opts = Hash[remember_cookie_options]
opts[:path] = "/" unless opts.key?(:path)
::Rack::Utils.delete_cookie_header!(response.headers, remember_cookie_key, opts)
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 update_remember_cookie
return if request.cookies[multiple_remember_cookie_key]
account_id, key = request.cookies[remember_cookie_key].to_s.split('_', 2)
return unless account_id && key
actual, deadline = db[remember_table]
.where(remember_id_column=>account_id)
.where(Sequel.expr(remember_deadline_column) > Sequel::CURRENT_TIMESTAMP)
.get([remember_key_column, remember_deadline_column])
return unless actual
actual_matches_key = (hmac_secret && timing_safe_eql?(key, compute_hmac(actual))) || (!hmac_secret && timing_safe_eql?(key, actual))
if actual_matches_key
account_from_session
remember_login
end
end
def update_remember_token_to_key_based_remember_token?
request.cookies[remember_cookie_key]
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 get_remember_key
unless @remember_key_value = active_remember_keys_ds.get(multiple_remember_key_column)
generate_remember_key_value
add_remember_key
end
nil
end
def forget_login
opts = Hash[remember_cookie_options]
opts[:path] = "/" unless opts.key?(:path)
::Rack::Utils.delete_cookie_header!(response.headers, multiple_remember_cookie_key, opts)
end
attr_reader :remember_key_id
def add_remember_key
hash = {multiple_remember_account_id_column=>account_id, multiple_remember_key_column=>remember_key_value}
set_deadline_value(hash, multiple_remember_deadline_column, remember_deadline_interval)
@remember_key_id = remember_key_ds.insert(hash)
end
def remove_remember_key(id=account_id, key_id=remember_key_id)
remember_key_ds(id).where(multiple_remember_id_column=>key_id).delete
end
def remove_all_remember_keys(id=account_id)
remember_key_ds(id).delete
end
def logged_in_via_remember_key?
authenticated_by.include?('remember')
end
private
def _set_remember_cookie(account_id, key_id, remember_key_value, deadline)
opts = Hash[remember_cookie_options]
opts[:value] = "#{account_id}_#{key_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, multiple_remember_cookie_key, opts)
end
def set_remember_cookie
_set_remember_cookie(
account_id,
remember_key_id,
remember_key_value,
current_remember_key_ds(account_id, remember_key_id).get(multiple_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
return unless cookie = _get_remember_cookie
account_id, key_id, key = cookie.split('_', 3)
return unless account_id && key_id && key
actual, deadline = current_remember_key_ds(account_id, key_id).get([multiple_remember_key_column, multiple_remember_deadline_column])
return unless actual
if hmac_secret && timing_safe_eql?(key, compute_hmac(actual))
current_remember_key_ds(account_id, key_id).update(
multiple_remember_deadline_column=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, remember_period)
)
end
_set_remember_cookie(
account_id,
key_id,
actual,
deadline
)
set_session_value(remember_deadline_extended_session_key, Time.now.to_i) if extend_remember_deadline?
end
def account_from_remember_cookie
id = remembered_session_id
unless 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, remembered_session_key_id)
forget_login
return
end
account
end
def _get_remember_cookie
request.cookies[multiple_remember_cookie_key]
end
def after_logout
forget_current_remember_key
forget_login
super if defined?(super)
end
def forget_current_remember_key
return unless cookie = _get_remember_cookie
account_id, key_id, key = cookie.split('_', 3)
return unless account_id && key_id && key
actual, deadline = current_remember_key_ds(account_id, key_id).get([multiple_remember_key_column, multiple_remember_deadline_column])
return unless actual
if hmac_secret && timing_safe_eql?(key, compute_hmac(actual))
current_remember_key_ds(account_id, key_id).delete
end
end
def after_close_account
remove_all_remember_keys
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[multiple_remember_table].where(multiple_remember_account_id_column=>id)
end
def current_remember_key_ds(id=account_id, key_id)
active_remember_keys_ds(id).where(multiple_remember_id_column=>key_id)
end
def active_remember_keys_ds(id=account_id)
remember_key_ds(id).where(Sequel.expr(multiple_remember_deadline_column) > Sequel::CURRENT_TIMESTAMP)
end
end
end |
Beta Was this translation helpful? Give feedback.
-
Thanks for submitting the code you are using, I think it will be helpful to others.
This is only true if you set |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I was reading through the code, and found this surprising part:
rodauth/lib/rodauth/features/remember.rb
Lines 158 to 167 in 94123d8
The way I'm reading this, is that an account can only have one
remember token
active at any time. If a user logs into their account in an other browser, theremember token
of the other browser gets removed.Am I reading this correct? Is this a conscious design decision, and what is the rationale behind this?
Beta Was this translation helpful? Give feedback.
All reactions