Implement a passwordless authentication with Sorcery

Last updated on December 20, 2021

What is passwordless authentication?

Passwordless authentication is a verification process that determines whether someone is, in fact, who they say they are without requiring that person to enter their username and password.

Instead you use a security token to authenticate the user.

Here are some benefits of passwordless auth:

  • improved UX - the user doesn't need to remember passwords and request new password if they forget their old one
  • increased security - reduced chance of attacks, password re-use and leaks
  • relatively simple to create - you don't need to create a lot of views

What is Sorcery?

Even though that I am a big fan of Devise, I started to like how simple and clean Sorcery is. It doesn't take much code to get Sorcery up and running, especially if you are implementing passwordless authentication.

Authentication through email

This is the basic premise of using email to authenticate the user

  • a user inputs their email on an input link
  • we send an email with a magic link (one time, randomly generated code)
  • The user clicks the link and the service being used will identify the token and exchange it for a live token, logging the user in.

Enough theory, let's get to coding

Our goal in this article is to create a passwordless authentication system for the User model and send the user an email containing a magic link. When a user click on the magic link, they are automatically signed in.

Create a user controller and model

Pay attention to the login_token and the login_token_valid_until - we will use those to store information about the passwordless auth.

shell
1
rails g controller users index edit update
2
rails g model User name:string email:string login_token:string login_token_valid_until:datetime
3
rake db:migrate

Create the skeleton for the authentication logic

In these controllers, we will validate the token and login the user.

shell
1
rails g controller logins create
2
rails g controller sessions create destroy

Update routes file

ruby
1
Rails.application.routes.draw do
2
#
3
get 'sessions/create'
4
delete 'sessions/destroy'
5
6
#
7
get 'logins/new'
8
post 'logins/create'
9
10
#
11
resources :users
12
13
#
14
root 'users#index'
15
end

Configure Sorcery

Let's go to Github and see how to configure Sorcery, in order to link it to the User model.

We won't need to use all the steps in the example, because we don't want passwords.

In the Gemfile, include

ruby
1
gem 'sorcery'

and in the terminal execute the command

shell
1
bundle

Next, we hook Sorcery to the User model by simple writing

ruby
1
class User < ApplicationRecord
2
authenticates_with_sorcery!
3
end

Update Application Controller to configure current_user.

We also let rails know what should happen if the user is not authenticated successfully.

ruby
1
class ApplicationController < ActionController::Base
2
def user_class
3
User
4
end
5
6
def not_authenticated
7
redirect_to root_path, alert: 'Not authenticated'
8
end
9
end

Finally, include require_login in the UsersController in order to let Sorcery know which actions require authentication.

ruby
1
class UsersController < ApplicationController
2
before_action :require_login, only: [:edit, :update]
3
def index
4
end
5
6
def edit
7
end
8
9
def update
10
end
11
end

Create a form that sends an email

Install the simple form gem

ruby
1
gem 'simple_form'

In logins/new.html.erb, we define a simple form that submits an email address.

erb
1
<%= simple_form_for @user, url: logins_create_path, method: :post do |f| %>
2
<%= f.input :email, label: "Email" %>
3
<%= f.submit "Get a magic link" %>
4
<% end %>

Next, let's update our LoginsController to look like this

ruby
1
class LoginsController < ApplicationController
2
def new
3
@user = User.new
4
end
5
6
def create
7
# the user might already exist in our db or it might be a new user
8
user = User.find_or_create_by!(email: params[:user][:email])
9
10
# create a login_token and set it up to expiry in 60 minutes
11
user.update!(login_token: SecureRandom.urlsafe_base64,
12
login_token_valid_until: Time.now + 60.minutes)
13
14
# create the url which is to be included in the email
15
url = sessions_create_url(login_token: user.login_token)
16
17
# send the email
18
LoginMailer.send_email(user, url).deliver_later
19
20
redirect_to root_path, notice: 'Login link sent to your email'
21
end
22
end

Create the LoginMailer

We defined the LoginMailer, now we need to create it.

Create a file in app/mailers/login_mailer that will look like this

ruby
1
class LoginMailer < ApplicationMailer
2
def send_email(user, url)
3
@user = user
4
@url = url
5
6
mail to: @user.email, subject: 'Sign in into mywebsite.com'
7
end
8
end

We need to create the view of the letter. Create a new file app/views/login_mailer/send_email.html.erb

erb
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
5
</head>
6
<body>
7
<h1>Welcome <%= @user.email %>,</h1>
8
<a href="<%= @url %>">Magic link</a>
9
</html>

Full Sessions controller

ruby
1
class SessionsController < ApplicationController
2
def create
3
# we don't log in the user if a login token has expired
4
user = User.where(login_token: params[:login_token])
5
.where('login_token_valid_until > ?', Time.now).first
6
7
if user
8
# nullify the login token so it can't be used again
9
user.update!(login_token: nil, login_token_valid_until: 1.year.ago)
10
11
# sorcery helper which logins the user
12
auto_login(user)
13
14
redirect_to root_path, notice: 'Congrats. You are signed in!'
15
else
16
redirect_to root_path, alert: 'Invalid or expired login link'
17
end
18
end
19
20
def destroy
21
# sorcery helper which logouts the user
22
logout
23
24
redirect_to root_path, notice: 'You are signed out'
25
end
26
end

Congrats! You have a passwordless authentication

Final touches - UsersController and user views

Let's finish this article by actually doing something with the passwordless auth.

Update your UsersController which allows us to update the user name.

ruby
1
class UsersController < ApplicationController
2
before_action :require_login, only: [:edit, :update]
3
def index
4
end
5
6
def edit
7
end
8
9
def update
10
current_user.update! user_params
11
redirect_to root_path
12
end
13
14
private
15
def user_params
16
params.require(:user).permit(:name)
17
end
18
end

Update views/users/index.html.erb

erb
1
<% if current_user %>
2
<h1><%= current_user.name.present? ? "My name is #{current_user.name}" : "Input your name using the link below" %></h1>
3
<%= link_to "Edit current user", edit_user_path(current_user) %> <br>
4
<%= link_to "Sign out", sessions_destroy_url, method: :delete %> <br>
5
<% else %>
6
<h1>Login to input your name</h1>
7
<%= link_to "Login to edit", logins_new_path %>
8
<% end %>

and finally views/users/edit.html.erb

erb
1
<%= simple_form_for current_user do |f| %>
2
<%= f.input :name, label: "Name" %>
3
<%= f.submit "Update name" %>
4
<% end %>

To make sure that the authentication works, go to incognito and enter the edit page URL. You shouldn't be able to access the page unless you are logged in with the correct account.

Source code for this article.

Level up your web development skills

Get articles, guides and interviews right in your inbox. Join a community of fellow developers.

No spam. Unsubscribe at any time.