This article was originally publish on Build a SaaS by Rails Designer
Let me cut to the chase: I am not a fan of “passwordless” or “magic links”. It's slower, annoying and prone to pull you out of the zone (because that one email suddenly needed an answer). No, for me, a password manager is at least one factor faster. But, I am self-aware enough, to know that I am not the average web-app user. Also they're simpler to implement than you might think, and they solve a bunch of common authentication headaches for the common internet user.
This article builds on top of basic Rails 8 authentication. See all the previous commits in this repo.
First, you'll need a simple signup form. Here's what that looks like:
<%# app/views/magic_signups/new.html.erb %>
<%= form_with model: @signup, url: magic_signups_path do |form| %>
<%= form.email_field :email_address,
required: true,
autofocus: true,
autocomplete: "username",
placeholder: "Enter your email address" %><br>
<%= invisible_captcha %>
<%= form.submit "Sign up" %>
<% end %>
Nothing fancy—just an email field and a submit button. The invisible_captcha
is there to keep the bots away (more on that in my spam prevention article).
The real magic happens in the MagicSignin
class:
# app/models/magic_signin.rb
class MagicSignin
AUTO_GENERATED_PASSWORD = SecureRandom.hex(32)
include ActiveModel::Model
include ActiveModel::Attributes
attribute :email_address, :string
validates :email_address, presence: true
validates :email_address, is_not_spam: true
def save
return unless valid?
User.where(email_address: email_address).first_or_create.tap do |user|
if user.new_record?
user.password = AUTO_GENERATED_PASSWORD
user.save
create_workspace_for user
end
send_magic_link_to user
end
end
end
This class handles both new signups and existing users. If it's a new user, it creates an account with a random password (they'll never need it) and sets up their workspace. Either way, it sends them a magic link.
Speaking of magic links, you'll need to add token support to your User model:
# app/models/user.rb
class User < ApplicationRecord
+
+ generates_token_for :signin, expires_in: 5.minutes
end
This gives you secure, time-limited tokens for your magic links. Five minutes is usually enough time for users to click the link in their email.
The magic link email itself is straightforward:
# app/mailers/magic_signup_mailer.rb
class MagicSignupMailer < ApplicationMailer
def magic_link(user)
@user = user
mail to: @user.email_address
end
end
With a simple template:
<%# app/views/magic_signup_mailer/magic_link.html.erb %>
<p>
Here is your magic link: <%= magic_session_url(@user.generate_token_for(:signin)) %>
</p>
(of course you want to extend the message in the email a bit—we are not savages! 😅)
When users click that link, the MagicSessionsController
handles the verification:
# app/controllers/magic_sessions_controller.rb
class MagicSessionsController < ApplicationController
before_action :set_user_by_token, only: %i[ show ]
def show
start_new_session_for @user
redirect_to root_path
end
private
def set_user_by_token
@user = User.find_by_token_for(:signin, params[:token])
rescue ActiveSupport::MessageVerifier::InvalidSignature
redirect_to new_magic_signup_path, alert: "Link is invalid or has expired."
end
end
Don't forget to add the routes:
# config/routes.rb
Rails.application.routes.draw do
+
+ resources :magic_signups, path: "magic-signup", only: %w[new create]
+ resources :magic_sessions, path: "magic-session", param: :token
end
That's it! Your users can now sign up and sign in without ever creating a password. Magic links are, I must admit, for the average user, more secure than passwords in many ways: no weak passwords to crack, no password reuse across sites, and no forgotten password flows to maintain.
Want to see this in action? Check out the full example app on GitHub. Got questions or suggestions? Let me know.
Top comments (0)