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.