Custos

Remember Me

Long-lived sessions with persistent tokens

Remember Me Plugin

The Remember Me plugin creates long-lived tokens that allow users to stay signed in across browser sessions. This is the "Remember me" checkbox on a sign-in form.

Setup

class User < ApplicationRecord
  include Custos::Authenticatable

  custos do
    plugin :remember_me
  end
end

Run the model generator:

rails generate custos:model User remember_me
rails db:migrate

This creates the custos_remember_tokens table.

Configuration Options

custos do
  plugin :remember_me, remember_duration: 14 * 24 * 60 * 60  # 14 days
end
OptionTypeDefaultDescription
remember_durationInteger2592000 (30 days)How long the token remains valid, in seconds

Instance Methods

generate_remember_token

Creates a new persistent remember token for the user. Returns the plain-text token to be stored in a cookie.

token = user.generate_remember_token
# => "xYzAbC123..." (store in a persistent cookie)

forget_me!(token = nil)

Destroys remember tokens for the user. Without arguments, destroys all tokens (sign out everywhere). With a token, destroys only the matching one (sign out from a single device).

user.forget_me!          # destroy all tokens
user.forget_me!(token)   # destroy a specific token

custos_remember_tokens

An Active Record association returning all remember tokens:

user.custos_remember_tokens
# => [#<Custos::RememberToken id: 1, ...>]

Class Methods

authenticate_remember_token(token)

Verifies a remember token. Finds the token by its digest, checks that it hasn't expired, and returns the associated user. Returns nil if the token is invalid or expired.

user = User.authenticate_remember_token("xYzAbC123...")
# => #<User id: 1, ...>

Controller Example

Sign In with Remember Me

class SessionsController < ApplicationController
  def create
    user = User.find_by_email_and_password(
      email: params[:email],
      password: params[:password]
    )

    if user
      _session, session_token = Custos::SessionManager.create(user, request: request)
      cookies.signed[:custos_session_token] = {
        value: session_token,
        httponly: true,
        secure: Rails.env.production?
      }

      if params[:remember_me] == "1"
        remember_token = user.generate_remember_token
        cookies.signed[:custos_remember_token] = {
          value: remember_token,
          httponly: true,
          secure: Rails.env.production?,
          expires: 30.days.from_now
        }
      end

      redirect_to dashboard_path
    else
      flash[:alert] = "Invalid email or password."
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    custos_session&.then { |s| Custos::SessionManager.revoke(s) }
    custos_current&.forget_me!
    cookies.delete(:custos_session_token)
    cookies.delete(:custos_remember_token)
    redirect_to root_path
  end
end

Restoring Session from Remember Token

class ApplicationController < ActionController::Base
  before_action :restore_session_from_remember_token

  private

  def restore_session_from_remember_token
    return if custos_authenticated?

    remember_token = cookies.signed[:custos_remember_token]
    return unless remember_token

    user = User.authenticate_remember_token(remember_token)
    return unless user

    _session, session_token = Custos::SessionManager.create(user, request: request)
    cookies.signed[:custos_session_token] = {
      value: session_token,
      httponly: true,
      secure: Rails.env.production?
    }
  end
end

Database Schema

custos_remember_tokens

ColumnTypeNotes
idbigintPrimary key
authenticatable_typestringPolymorphic type
authenticatable_idbigintPolymorphic ID
token_digeststringHMAC-SHA256 digest, unique index
expires_atdatetimeWhen the token expires
created_atdatetimeCreation timestamp

Security Notes

  • Only the HMAC-SHA256 digest is stored; the plain-text token is set in a cookie once
  • Tokens have an explicit expiry (expires_at) checked server-side
  • forget_me! destroys all tokens, effectively signing out from all remembered sessions
  • The cookie should always be set with httponly: true and secure: true in production

On this page