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

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_magic_login_path, notice: 'Please check your inbox')
else
redirect_to(new_magic_login_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
``` ```ruby
Rails.application.routes.draw do
resources :magic_logins, only: %i[new create]
end
Let's add a view for the new action. I'm not going to style this, you can style however you like, we'll simply add a form for an example.
<%= form_for(User.new, url: magic_logins_path, method: :post do %>
<%= f.label :email %>
<%= f.email_field :email %>
<%= f.submit 'Send me a magic login link' %>
<% 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
``` ```erb
<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
``````ruby
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
``` ```ruby
Rails.application.routes.draw do
resources :magic_logins, only: %i[index new create]
end
That's all there is to it. It's really quite simple to implement.


