DEV Community

Rails Designer
Rails Designer

Posted on • Originally published at railsdesigner.com

2

Adding Magic Links to Rails 8 Authentication

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 %>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)