Magic Link Authentication

Overview

The app supports two login methods side by side:

  1. Password login — the traditional Devise email + password flow (unchanged)
  2. Magic link login — the user enters only their email and receives a one-time sign-in link via email (no password required)

Both methods produce the same authenticated session and work with the full Devise feature set (remember me, Pundit authorization, multi-tenancy, etc.).


How It Works

1. User visits /users/sign_in
2. Clicks "Sign in with a magic link"
3. Redirected to /users/magic_link_login (email form)
4. User enters their email and submits
5. App sends a signed, time-limited token to the user's email
6. User clicks the link in the email → GET /users/magic_link?token=...
7. App validates the token and signs the user in
8. User is redirected to their dashboard

Token Properties

  • Stateless — tokens are cryptographically signed using Rails GlobalID (no database column needed)
  • Single scope — tied to the user's current password hash; changing a password invalidates all outstanding magic links
  • Expiry — tokens are valid for 20 minutes (configured in config/initializers/devise.rb)
  • Security — the :token parameter is already filtered from logs via config/initializers/filter_parameter_logging.rb

Gem

devise-passwordless v1.1.0

Added to Gemfile:

gem "devise-passwordless"

Files Changed / Created

Modified Files

File Change
Gemfile Added devise-passwordless gem
app/models/concerns/user/authentication.rb Added :magic_link_authenticatable to Devise modules
config/routes.rb Added magic_links: controller to devise_for; added devise_scope for magic link request form
config/initializers/devise.rb Added Devise::Passwordless::Mailer, tokenizer, and login_within config
config/locales/devise.en.yml Added I18n keys for passwordless and mailer.magic_link
app/views/devise/sessions/new.html.erb Added divider and "Sign in with a magic link" button

New Files

File Purpose
app/controllers/users/magic_links_controller.rb Validates magic link token from email click-through
app/controllers/users/magic_link_sessions_controller.rb Handles the email entry form (sends the magic link)
app/views/users/magic_link_sessions/new.html.erb Email entry form (request a magic link)
app/views/devise/mailer/magic_link.html.erb HTML email template with the magic link
app/views/devise/mailer/magic_link.text.erb Plain text email template

Routes

GET  /users/sign_in            → users/sessions#new       (password login)
POST /users/sign_in            → users/sessions#create    (password login submit)

GET  /users/magic_link_login   → users/magic_link_sessions#new    (magic link email form)
POST /users/magic_link_login   → users/magic_link_sessions#create (sends the magic link)

GET  /users/magic_link         → users/magic_links#show   (validates token, signs in)

Configuration

In config/initializers/devise.rb:

# Use the Passwordless mailer to send magic link emails
config.mailer = "Devise::Passwordless::Mailer"

# Token algorithm — stateless, uses Rails GlobalID (no DB storage)
config.passwordless_tokenizer = "SignedGlobalIDTokenizer"

# How long a magic link is valid after it's sent
config.passwordless_login_within = 20.minutes

Customization

Changing Token Expiry

Edit config/initializers/devise.rb:

config.passwordless_login_within = 30.minutes  # or 1.hour, etc.

Override in the User model:

def active_for_magic_link_authentication?
  super && some_condition?
end

Disabling Password Login for Specific Users

def active_for_authentication?
  super && some_condition?
end

Custom After-Sign-In Redirect

The magic link sign-in follows the same after_sign_in_path_for hook used by password login. Override it in ApplicationController if needed.


Email Template

The email is sent via Devise::Passwordless::Mailer. The templates live at:

  • app/views/devise/mailer/magic_link.html.erb (HTML)
  • app/views/devise/mailer/magic_link.text.erb (plain text)

The email subject is set in config/locales/devise.en.yml:

devise:
  mailer:
    magic_link:
      subject: "Your magic sign-in link"

Testing

Manual Test

  1. Start the server: bin/dev
  2. Open http://localhost:3000/users/sign_in
  3. Click "Sign in with a magic link"
  4. Enter a registered user's email
  5. Check the Rails logs for the magic link URL (development uses log delivery)
  6. Copy and open the link → should sign you in
  7. Verify password login still works at /users/sign_in

Verify Token Expiry

In development, temporarily change config.passwordless_login_within = 1.second, submit the form, wait 2 seconds, then click the link. You should see an "Invalid or expired magic link" flash message.