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
endRun the model generator:
rails generate custos:model User magic_link
rails db:migrateThis 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| Option | Type | Default | Description |
|---|---|---|---|
expiry | Integer | 900 (15 min) | Token lifetime in seconds |
cooldown | Integer | 60 (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
endauthenticate_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
endMailer 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
endDatabase Schema
custos_magic_links
| Column | Type | Notes |
|---|---|---|
id | bigint | Primary key |
authenticatable_type | string | Polymorphic type |
authenticatable_id | bigint | Polymorphic ID |
token_digest | string | HMAC-SHA256 digest, unique index |
expires_at | datetime | When the token expires |
used_at | datetime | Set when token is consumed |
created_at | datetime | Creation 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_linkmethod returnsnilfor 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