How to Implement Passwordless Authentication in Ruby on Rails

We recently discussed the benefits of passwordless authentication. Now let's take a look at how to implement passwordless authentication in Ruby on Rails

Frameworks and Languages

In the previous article I discussed the benefits of passwordless authentication. In this article I'm going to demonstrate how to implement magic sign in link emails in Ruby on Rails. For those unfamiliar, magic login links allow a user to log into their account without entering a password. Instead of the traditional email and password form, the user only needs to enter their email address. The system will then send a link to the user which contains a token linked to their account. When the user clicks the link they will be taken to the application and seamlessly logged into their account. With this technique we can even log them into native applications through the usage of deeplinks.

Let's Write Some Code


The first thing we need to do is add a new column to our Users to store the login tokens. We'll use UUIDs to make generation simpler.

rails g migration add_login_token_to_users login_token:uuid

Now let's create a controller to handle the log ins. This controller won't be strictly RESTful as we can't use a post request from the users email client. I normally use magic_logins_controller so we can also reserve sessions controller for password logins should we ever want to have both. 

class MagicLoginsController < ApplicationController
  def new; end

  def create
    if (user = User.find_by(login_token_params))
      user.generate_login_token!
      LoginMailer.with(user: user).magic_login_email.deliver_later
      redirect_to(new_login_token_path, notice: 'Please check your inbox')
    else
      redirect_to(new_login_token_path, alert: 'It looks like that email is not registered')
    end
  end

  private def login_token_params
    params.require(:login_token).permit(:email)
  end
end

Rails.application.routes.draw do
  resources :magic_logins, only: %i[new create]
end

Next we need to add a method to generate a new token to our User model.

class User < ApplicationRecord
  def generate_login_token!
    update_column(:login_token, SecureRandom.uuid)
  end
end

We don't need to validate anything here as we're using UUIDs so we should be safe. Ok, so next, let's create our magic_login_email.

class LoginMailer < ApplicationMailer
  def magic_login_email
    user = params[:user]
    @magic_login_url = magic_logins_url(token: user.login_token)
    mail(to: user.email, subject: "Here's your login link")
  end
end

<h1>Here's your login link</h1>
<%= link_to 'Log in now', @magic_login_url %>

You might have noticed we're using the index url and passing the token as a query string. You can do this other ways but this approach is simple and fits in with what Rails already offers. It does of course violate RESTful principles so you might prefer to use another approach. 

Now we just need to add the logic to sign in the users. I'm going to assume you're using Devise. If you're using another gem or are managing sessions manually you'll need to adjust this logic.

class MagicLoginsController < ApplicationController
  def index
    redirect_to root_path unless params[:token].present?

    user = User.find_by(login_token: params[:token])
    if user
      sign_in(user)
      user.update_column(:login_token, nil)
      redirect_to after_sign_in_path, notice: 'Logged in successfully!'
    else
      redirect_to new_magic_login_path, alert: 'The link you used was invalid. Please request a new login link'
    end
  end
end

As you can see we are signing in the user and then removing the login token from the database. This prevents the token from being used more than once. Everything is all hooked up and users can now login using magic links. You may also like to restrict how long the tokens are valid for. To do this we can simply add another column to track when the token was created and verify it hasn't expired

rails g migration add_login_token_generated_at_to_users login_token_generated_at:datetime


class MagicLoginsController < ApplicationController
  def index
    redirect_to root_path unless params[:token].present?

    user = User.find_by(login_token: params[:token])
    if user
      if user.login_token_generated_at < 15.minutes.ago
        redirect_to new_magic_login_path, alert: 'The link you used has expired. Please request a new login link'
      end
      sign_in(user)
      user.update_column(:login_token, nil)
      redirect_to after_sign_in_path, notice: 'Logged in successfully!'
    else
      redirect_to new_magic_login_path, alert: 'The link you used was invalid. Please request a new login link'
    end
  end
end

That's all there is to it. It's really quite simple to implement.

Looking for a new challenge? Join Our Team

Like 1 like
Joe Woodward
I'm Joe Woodward, a Ruby on Rails fanatic working with OOZOU in Bangkok, Thailand. I love Web Development, Software Design, Hardware Hacking.
Share:

Join the conversation

This will be shown public
All comments are moderated

Comments

Nico
April 22nd, 2021
Thanks for this good written article !
However I'm still puzzled : what if someone intercept the email (or whatever, forwarded, click on someone else's device...) ?

Should'nt we manage a two part token ? First part sent by email on request, while the other part is part of request response and therefore stored on the device the request has been made ?

Get our stories delivered

From us to your inbox weekly.