Devise serialize into session trick
I always thought Devise is a great example of a remarkably flexible and modular behaviour implementation. To a level it is not fully documentable. Instead, after you got started with the basics, its source code serves best as documentation (and is a good read overall).
One of the many extension points which has attracted my attention before is serialize_into_session / serialize_from_session resource class methods.
As of now, the default implementations of these methods are:
def serialize_into_session(record)
[record.to_key, record.authenticatable_salt]
end
def serialize_from_session(key, salt)
record = to_adapter.get(key)
record if record && record.authenticatable_salt == salt
end
So - to put things simply - it serializes Devise resource instance (i.e. user
account) into a unique database identifier and vice versa. And the
authenticatable_salt
part is used to force close all sessions if user changes
his/her password.
Both are then called by Warden:
warden_config.serialize_into_session(mapping.name) do |record|
mapping.to.serialize_into_session(record)
end
warden_config.serialize_from_session(mapping.name) do |args|
mapping.to.serialize_from_session(*args)
end
Notice that Devise does not really care about serialize_from_session
argument number or structure. Whatever is returned by serialize_into_session
it will be passed to serialize_from_session
.
This means that by overriding both we can serialize Devise resource into a vector of information containing more than just primary database key. We can pretty much put all the user account attributes into the session and extra. Extras could be an identity provider name used to start the session, two factor authentication flag / details and similar.
By overriding these methods we can also implement anonymous ephemeral user session without persisting it to database. The implementation of this could look like this:
class Account < ApplicationRecord
devise :database_authenticatable, :registerable
class << self
def serialize_into_session(record)
if record.persisted?
[record.to_key, record.authenticatable_salt]
else
[record.to_key, record.authenticatable_salt, { attributes: record.attributes }]
end
end
def serialize_from_session(key, salt, opts = {})
if opts.key?("attributes")
new(opts["attributes"])
else
record = to_adapter.get(key)
record if record && record.authenticatable_salt == salt
end
end
end
end
Start of anonymous session then becomes something like this:
account = Account.new(id: SecureRandom.uuid, name: 'Nameless Account')
sign_in account
redirect_to after_sign_in_path_for(account)
After the “nameless user” decides to actually sign up, code could be something like this:
current_account.name = params[:name]
current_account.email = params[:email]
current_account.password = params[:password]
current_account.password_confirmation = params[:password_confirmation]
current_account.save
sign_in current_account, force: true
redirect_to after_sign_in_path_for(current_account)
Writing actual application code does require some mindset adjustment when
current_account
can be both incomplete instance and persisted record. Other
than that it works rather well. Nice effect of this is unified tracking /
logging of both anonymous and registered users without breakup around the
registration.