Magic Link Authentication
Overview
The app supports two login methods side by side:
- Password login β the traditional Devise email + password flow (unchanged)
- 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
Magic Link Flow
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
:tokenparameter is already filtered from logs viaconfig/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.
Disabling Magic Link for Specific Users
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
- Start the server:
bin/dev - Open
http://localhost:3000/users/sign_in - Click "Sign in with a magic link"
- Enter a registered user's email
- Check the Rails logs for the magic link URL (development uses log delivery)
- Copy and open the link β should sign you in
- 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.