Custos

Magic Link

Passwordless authentication via email links

Magic Link Plugin

The Magic Link plugin enables passwordless authentication. Users receive a one-time link via email that signs them in directly -- no password required.

Setup

class User < ApplicationRecord
  include Custos::Authenticatable

  custos do
    plugin :magic_link

    on(:magic_link_created) do |record, token|
      AuthMailer.magic_link(record, token).deliver_later
    end
  end
end

Run the model generator:

rails generate custos:model User magic_link
rails db:migrate

This creates the custos_magic_links table.

Configuration Options

custos do
  plugin :magic_link,
    expiry: 10 * 60,   # 10 minutes
    cooldown: 120       # 2 minutes between requests
end
OptionTypeDefaultDescription
expiryInteger900 (15 min)Token lifetime in seconds
cooldownInteger60 (1 min)Minimum interval between magic link requests per user (in seconds). Set to 0 to disable

Class Methods

generate_magic_link(email)

Finds the user by email, generates a secure token, stores the digest, and fires the :magic_link_created callback. Returns the plain-text token, or nil if no user is found with that email.

token = User.generate_magic_link("alice@example.com")
# => "aBcDeFgHiJ..." (plain-text token)

The callback receives the user record and the plain-text token so your mailer can construct the link:

on(:magic_link_created) do |record, token|
  AuthMailer.magic_link(record, token).deliver_later
end

authenticate_magic_link(token)

Verifies a magic link token. Finds the token by its digest, checks that it hasn't expired or been used, marks it as used, and returns the associated user. Returns nil if the token is invalid, expired, or already consumed.

user = User.authenticate_magic_link(params[:token])

Controller Example

class MagicLinksController < ApplicationController
  def create
    User.generate_magic_link(params[:email])
    # Always show success to prevent email enumeration
    redirect_to root_path, notice: "If that email exists, a sign-in link has been sent."
  end

  def show
    user = User.authenticate_magic_link(params[:token])

    if user
      _session, token = Custos::SessionManager.create(user, request: request)
      cookies.signed[:custos_session_token] = {
        value: token,
        httponly: true,
        secure: Rails.env.production?
      }
      redirect_to dashboard_path
    else
      redirect_to root_path, alert: "Invalid or expired link."
    end
  end
end

Mailer Example

class AuthMailer < ApplicationMailer
  def magic_link(user, token)
    @user = user
    @url = magic_link_url(token: token)
    mail(to: user.email, subject: "Your sign-in link")
  end
end

Database Schema

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

Security Notes

  • Only the HMAC-SHA256 digest of the token is stored in the database
  • Tokens are single-use: once verified, they are marked as used and cannot be reused
  • Default expiry is 15 minutes
  • The generate_magic_link method returns nil for nonexistent emails, but the controller should always show a generic success message to prevent email enumeration
  • Built-in cooldown (default: 60 seconds) prevents email bombing attacks

On this page