Tadas Sasnauskas Tech/Engineering Blog

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.