With the increasing number of daily routine mobile/web applications people use, password management becomes an issue. Almost all applications now provide social login, but some people would prefer not to use that method.
Account hacking and password leaking are also major concerns in the technical world. This article will guide you through implementing a login/registration system that does not require a password. Rather it will send you an email with a confirmation token every time you try to login. No memorization required, no password to leak, no hacking.
These are the key takeaways from this article:
- Overriding Devise views
- Overriding Devise controllers
- Implementing an authentication strategy with Devise
Many people are familiar with the first two, but the third is seen less often, because not all applications require customized authentication systems.
If you are starting from scratch and devise gem is not yet added to the Rails application, go ahead and follow the instructions on their official documentation. Before migrating the database, uncomment the following lines from db/migrate/20200411090959_devise_create_users.rb
:
1
2
3
4
t.string :confirmation_token
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
t.string :unconfirmed_email # Only if using reconfirmable
Also add confirmable to app/models/user.rb
.
This article is based on the user of the user model, so at this point you should have integrated devise using the user model. Now, override a method that will allow you to register a user without a password. In app/models/user.rb
:
1
2
3
def password_required?
false
end
Next, add token fields to the user model in your terminal.
1
rails generate migration add_token_fields_to_user
In your db/migrate/abcdxyz_add_token_fields_to_user.rb
:
1
2
3
4
5
6
class AddTokenFieldsToUser < ActiveRecord::Migration[6.0]
def change
add_column :users, :login_token, :string
add_column :users, :login_token_valid_until, :datetime
end
end
Now add a strategy, passwordless_authenticatable, in your terminal.
1
2
3
4
5
mkdir lib/devise
mkdir lib/devise/models
mkdir lib/devise/strategies
touch lib/devise/models/passwordless_authenticatable.rb
touch lib/devise/strategies/passwordless_authenticatable.rb
Now in your lib/devise/models/passwordless_authenticatable.rb
, add this:
1
2
3
4
5
6
7
8
9
require Rails.root.join('lib/devise/strategies/passwordless_authenticatable')
module Devise
module Models
module PasswordlessAuthenticatable
extend ActiveSupport::Concern
end
end
end
In lib/devise/strategies/passwordless_authenticatable.rb
, add this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
require 'devise/strategies/authenticatable'
require_relative '../../../app/mailers/user_mailer'
module Devise
module Strategies
class PasswordlessAuthenticatable < Authenticatable
def authenticate!
if params[:user].present?
user = User.find_by(email: params[:user][:email])
if user&.update(login_token: SecureRandom.hex(10),
login_token_valid_until: Time.now + 60.minutes)
url = Rails.application.routes.url_helpers.email_confirmation_url(login_token: user.login_token)
UserMailer.validate_email(User.first, url).deliver_now
fail!("An email was sent to you with a magic link.")
end
end
end
end
end
end
Warden::Strategies.add(:passwordless_authenticatable, Devise::Strategies::PasswordlessAuthenticatable)
Now in config/initializers/devise.rb
, add this.
1
2
3
4
5
6
Devise.add_module(:passwordless_authenticatable, {
strategy: true,
controller: :sessions,
model: 'devise/models/passwordless_authenticatable',
route: :session
})
Don’t worry about line number 2 and 15 for now, the mailer will be added in further steps. Here, line number 17 is worth noticing - authentication is failed because you don’t want to let the user login without confirming through email.
In config/environments/development.rb
:
1
Rails.application.routes.default_url_options = { host: 'http://localhost:3000' }
Now add this strategy in app/models/user.rb
file - it should look something like this:
1
2
3
4
5
6
7
8
9
10
11
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:confirmable, :passwordless_authenticatable
def password_required?
false
end
end
Override the sessions_controller that comes from devise. To do that, first generate your controller.
1
rails g devise:controllers users -c=sessions
Add two methods in app/controllers/users/sessions_controller.rb
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# frozen_string_literal: true
class Users::SessionsController < Devise::SessionsController
def sign_in_with_token
user = User.find_by(login_token: params[:login_token])
if user.present?
user.update(login_token: nil, login_token_valid_until: 1.year.ago)
sign_in(user)
redirect_to root_path
else
flash[:alert] = 'There was an error while login. Please enter your email again.'
redirect_to new_user_session_path
end
end
def redirect_from_magic_link
@login_token = params[:login_token] if params[:login_token].present?
end
end
In config/routes.rb
, add the following.
1
2
3
4
5
6
7
8
9
devise_for :users, controllers: {
sessions: 'users/sessions',
registrations: 'users/registrations'
}
devise_scope :user do
get 'email_confirmation', to: 'users/sessions#redirect_from_magic_link'
post 'sign_in_with_token', to: 'users/sessions#sign_in_with_token'
end
Let’s also generate views. First, add this line to config/initializers/devise.rb
:
1
config.scoped_views = true
Now in your terminal:
1
rails generate devise:views users
Now remove all the password related fields from all the views, and create a new file app/views/users/sessions/redirect_from_magic_link.html.erb
.
1
2
3
4
5
6
7
8
9
10
11
12
13
<h2>Log in</h2>
<%= form_tag('/sign_in_with_token') do %>
<div class="field">
<%= hidden_field_tag :login_token, @login_token %>
</div>
<div class="actions">
<%= submit_tag "Log in" %>
</div>
<% end %>
<%= render "users/shared/links" %>
Next it’s time to add a mailer.
1
rails g mailer user
In app/mailers/user_mailer.rb
, add this:
1
2
3
4
5
6
7
class UserMailer < ApplicationMailer
def validate_email(user, url)
@user = user
@url = url
mail to: @user.email, subject: 'Sign in into mywebsite.com'
end
end
Create app/views/user_mailer/validate_email.html.erb
:
1
2
<p><%= @user.email %></p>
<%= link_to "Confirm", @url %>
To catch email, configure letter opener in development. Follow this guide to add letter opener to your project.
Now you have successfully added passwordless authentication in your Rails application. Happy authenticating!