Build Magic link authentication for Devise natively in Rails 7.1 thanks to generates_token_for
Guillaume Briday
3 minutes
Today, I decided to implement a Magic Link authentication system for my SaaS app, Timecop-app. The app’s authentication is powered by Devise and Rails 7.2.
Rails 7.1 introduced several new features, but the one we're focusing on here is the generates_token_for
method.
This feature is particularly useful because it lets us create signed tokens with expiration dates, all without needing to modify the database.
While there are many gems available that add passwordless authentication to Devise, I chose not to install any additional gems. With Rails' native capabilities, it's now straightforward to implement this functionality without extra dependencies, which is awesome!
Many of these gems haven’t been updated in months or years, and some come with unnecessary complexity. However, if you're looking for a gem with extra features, devise-passwordless is probably the best option.
Configure your model
As mentioned earlier, there's no need to update the database. You simply need to implement the generates_token_for
method in the User
model:
# app/models/user.rb
class User < ActiveRecord::Base
generates_token_for :magic_login, expires_in: 15.minutes do
# The magic_login token will expire every time the user signs in.
last_sign_in_at
end
scope :confirmed, -> { where.not(confirmed_at: nil) }
end
This setup ensures that the magic link token expires 15 minutes after it's generated or when the user signs in, whichever comes first.
Implement the logic in the controller
Next, we'll implement a controller to handle the logic for Magic Link authentication.
The process is straightforward and involves setting up a basic CRUD controller with a show
method to sign in the user, a new
method to display the form, and a create
method to send the token via a MagicLoginMailer
.
Here’s how you can set it up:
# app/controllers/magic_logins_controller.rb
class MagicLoginsController < ApplicationController
def new
end
def create
user = User.confirmed.find_by(email: params[:email]) # Restrict search to confirmed users to prevent spam.
MagicLoginMailer.login(user).deliver_later if user.present?
redirect_to new_magic_logins_path, notice: t('devise.magic_logins.successful.created')
end
end
Add the form to your views
Now, add the form to your views to generate and send the magic link:
<%# app/views/magic_logins/new.html.erb %>
<%= form_with url: magic_logins_path do |f| %>
<%= f.label :email %>
<%= f.email_field :email,
required: true,
autocomplete: 'email' %>
<%= f.submit t('devise.sessions.new.send_magic_link') %>
<% end %>
This setup allows users to request a magic link by entering their email, which is then processed and sent to them via email.
Update the routes
Finally, ensure your routes are configured correctly by adding the resource.
Since we're dealing with a single token without an /:id
, the resource must be singular:
resource :magic_logins, only: %i[show new create]
Implement throttling to mitigate spam
To limit abusive requests, you can utilize the rack-attack gem for request throttling. Add the following configuration to throttle the new endpoint and help prevent spam:
# config/initializers/rack_attack.rb
class Rack::Attack
throttle('magic_logins/create', limit: 5, period: 15.minutes) do |request|
request.ip if request.path == '/magic_logins' && request.post?
end
end
Send the Magic Link via email
To send the magic link to users, let's create a Mailer that handles this process:
# app/mailers/magic_login_mailer.rb
class MagicLoginMailer < ApplicationMailer
def login(user)
@user = user
@token = user.generate_token_for(:magic_login) # Generate the temporary token
mail(to: user.email, subject: t('magic_login_mailer.login.subject'))
end
end
Create the mailer's views
Next, create the view that will be used in the email sent to the user:
# app/views/magic_login_mailer/login.html.erb
<%= link_to magic_logins_url(token: @token) do %>
<%= t('devise.sessions.new.sign_in_with_magic_link') %>
<% end %>
Sign in the user with Devise
To complete the Magic Link authentication, we'll implement the show
method in our MagicLoginsController
to sign in the user when they click the link:
# app/controllers/magic_logins_controller.rb
class MagicLoginsController < ApplicationController
def show
user = User.find_by_token_for(:magic_login, params[:token])
if user.present?
sign_in(user) # This will invalidate the token by updating `last_sign_in_at`.
redirect_to root_path, notice: t('devise.sessions.signed_in')
else
flash[:alert] = t('devise.failure.magic_link_expired')
redirect_to new_magic_logins_path
end
end
# ...
end
And that's it! It's as simple as that.
With Rails 7.1+, there's no need for extra gems that may be outdated or overly complex. The built-in features make setting up Magic Link authentication straightforward and efficient.