ruby 116 lines · 3 tabs

Signed 'Remember Me' Cookies for Persistent Login in Sinatra

Shared by codesnips Sep 2026
3 tabs
require 'securerandom'
require 'digest'
require 'openssl'
require 'rack/utils'

class RememberToken < Sequel::Model
  SIGNING_KEY = ENV.fetch('REMEMBER_SIGNING_KEY')

  def self.issue(user_id, ttl: 365 * 24 * 3600)
    selector  = SecureRandom.urlsafe_base64(12)
    validator = SecureRandom.urlsafe_base64(24)
    create(
      user_id: user_id,
      selector: selector,
      validator_digest: Digest::SHA256.hexdigest(validator),
      expires_at: Time.now + ttl
    )
    { selector: selector, validator: validator }
  end

  def self.authenticate(selector, validator)
    token = where(selector: selector).first
    return nil unless token && token.expires_at > Time.now
    expected = token.validator_digest
    given    = Digest::SHA256.hexdigest(validator)
    return nil unless Rack::Utils.secure_compare(expected, given)
    token.user_id
  end

  def self.cookie_value(selector, validator)
    payload = "#{selector}:#{validator}"
    sig = OpenSSL::HMAC.hexdigest('SHA256', SIGNING_KEY, payload)
    "#{payload}:#{sig}"
  end

  def self.parse(raw)
    return nil unless raw
    selector, validator, sig = raw.split(':', 3)
    return nil unless selector && validator && sig
    payload  = "#{selector}:#{validator}"
    expected = OpenSSL::HMAC.hexdigest('SHA256', SIGNING_KEY, payload)
    return nil unless Rack::Utils.secure_compare(expected, sig)
    [selector, validator]
  end
end
3 files · ruby Explain with highlit

This snippet builds a persistent 'remember me' login for a Sinatra app using signed, tamper-evident cookies. The core idea is that a login form can drop a long-lived cookie that survives browser restarts, but that cookie must not be forgeable: if a client could edit the user id inside it, they could impersonate anyone. The solution stores a random selector plus a hashed validator server-side, and signs the cookie payload with an HMAC so the server can detect tampering before it ever hits the database.

In RememberToken model, each remember token is split into a public selector and a secret validator. Only the SHA-256 digest of the validator is persisted in validator_digest, so a leaked database row cannot be replayed directly. RememberToken.issue mints a fresh pair with SecureRandom.urlsafe_base64 and returns the raw values to the caller exactly once. authenticate looks the row up by selector, checks expiry, and uses Rack::Utils.secure_compare for a constant-time comparison to avoid timing attacks. The cookie value is built by cookie_value and verified by parse using OpenSSL::HMAC, again with secure_compare, so a mismatched signature is rejected without a DB hit.

In app.rb, the /login route calls RememberToken.issue and writes the signed value through response.set_cookie with httponly and secure flags plus a one-year max_age; these flags keep the cookie out of JavaScript and off plain HTTP. The before filter reads request.cookies['remember'], runs it through RememberToken.parse and authenticate, and hydrates @current_user on success while silently clearing a bad cookie. /logout both deletes the DB row and calls response.delete_cookie so the token can never be reused.

The SIGNING_KEY is loaded from the environment because the whole scheme collapses if the key leaks or is hardcoded. A subtle trade-off worth noting: signed cookies prove integrity, not confidentiality, so the validator still lives only as a digest server-side. Rotating tokens on each use would harden this further against theft, at the cost of extra writes. This pattern is what a developer reaches for when sessions alone are too short-lived but storing a bare user id in a cookie is unacceptable.


Related snips

Share this code

Here's the card — post it anywhere.

Signed 'Remember Me' Cookies for Persistent Login in Sinatra — share card
Link copied