Ruby
Article
By Vinoth

Password-Less Authentication in Rails

By Vinoth

Authentication is one of the key elements of many web applications. It is the stone wall between the application and its users, so it’s important that the authentication approach is secure and easy to use. But, what is this “authentication”? It’s a way of ensuring only users authorized by our application are allowed to use the application. As I am sure you know, there are many ways to authenticate a user, such as Email/Password, OpenID Connect, SAML, SSO, and so on.

Today, we’re going to take a look at another approach: Password-less authentication.

What is Password-less Authentication?

When a user registers for a website, the application allows the user to chose their credentials, usually a username or email/password. The user can then enter those credentials anytime to login to the application. Password-less authentication is basically eliminating the password part and using just the email to both register and login.

How this works is, when a user registers to our application, an email is sent to activate their account. This allows us to verify if the email belongs to the user. Now that we have a verified email, the next time that user tries to login, we will send them an email with the token for the user to use to sign into the app. Once the user clicks on the link with the token, the application will authenticate the user.

Let’s get started. Initialize your rails application:

$ rails new passwordless
$ cd passwordless

To get a clearer sense of what’s really happening, we won’t be using any libraries or gems for this tutorial.

Creating the Model

Let’s start with creating the model necessary for our application. We are going to call this model User since it’s users we are authenticating. But you are free to use anything that works.

$ rails g scaffold user fullname username:uniq email:uniq login_token token_generated_at:datetime
$ rails db:create && rails db:migrate

The fullname and username are optional. We’ve added username to enable users to login via either email or username. We also have couple of other columns, login_token and token_generated_at, which are basically the one time password we generate for our users, along with when it was generated.

There’s a unique constraint at the table level, but let’s also add the ActiveRecord validations for the model. Add the following to the app/models/user.rb:

validates :email, :username, uniqueness: true, presence: true

Along with this, let’s also add a before filter to format the username and email before saving the record. These validations should also be in the client, but this is an added measure. Add the before filter:

...
before_save :format_email_username

def format_email_username
  self.email = self.email.delete(' ').downcase
  self.username = self.username.delete(' ').downcase
end

Here we basically strip the spaces in username and email and making it lowercase before saving it to the database.

Since we’re going to allow for users to login via username and email, let’s add a helper method that fetches the user record based on either:

...

def self.find_user_by(value)
  where(["username = :value OR email = :value", {value: value}]).first
end

Registration

With the model done and in place, let’s go ahead and create our controller file and the necessary routes. Before we go about creating the registration controller, quickly create a static page to show messages:

$ rails g controller static home

Add the below route to config/routes.rb:

root 'static#home'

Along with this, also add the following line to your app/views/layouts/application.html.erb, which is used to display messages to the user:

...
<body>
  <p id="notice"><%= notice %></p>
...

Generate a users controller:

$ rails g controller users

In config/routes.rb, add the following routes which we’ll use for user registration:

resources :users, only: [:create]
get '/register', to: 'users#register'

Now, let’s add the code in app/controllers/users_controller.rb that corresponds to the routes declared above:

def register
  @user = User.new
end

def create
  @user = User.new(user_params)

  if @user.save
    redirect_to root_path, notice: 'Welcome! We have sent you the link to login to our app'
  else
    render :register
  end
end

private

def user_params
  params.require(:user).permit(:fullname, :username, :email)
end

Now, create the view file for registration under app/views/users/register.html.erb and add the this form to it:

<h1>Register</h1>

<%= form_for(@user) do |f| %>
  <% if @user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@user.errors.count, "error") %> prohibited this @user from being saved:</h2>

      <ul>
      <% @user.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :fullname %>
    <%= f.text_field :fullname %>
  </div>

  <div class="field">
    <%= f.label :username %>
    <%= f.text_field :username %>
  </div>

  <div class="field">
    <%= f.label :email %>
    <%= f.text_field :email %>
  </div>

  <div class="actions">
    <%= f.submit 'Register' %>
  </div>
<% end %>

Noting special in this form. This is a standard Rails, scaffole-generated form which captures the fullname, username, and email for the user and sends it to our create endpoint. Start the Rails server and head over to /register and see the registration is live now!

--ADVERTISEMENT--

Login Link

Let’s get to the meaty part of the application: sending login emails. Basically, when a new user registers or whenever they request to login, we’d have to send them a login link with a token. When the link is clicked, the app will login the user. Begin by adding the following helper methods to our apps/models/user.rb for sending emails:

...

  def send_login_link
    generate_login_token

    template = 'login_link'
    UserMailer.send(template).deliver_now
  end

  def generate_login_token
    self.login_token = generate_token
    self.token_generated_at = Time.now.utc
    save!
  end

  def login_link
    "http://localhost:3000/auth?token=#{self.login_token}"
  end

  def login_token_expired?
    Time.now.utc > (self.token_generated_at + token_validity)
  end

  def expire_token!
    self.login_token = nil
    save!
  end


  private

  def generate_token
    SecureRandom.hex(10)
  end

  def token_validity
    2.hours
  end
end

Please don’t get hung up on where the code that sends the mail lives. I am trying to keep the noise down as much as possible and focus on the higher-level concepts. I would never put a UserMailer, for example, in a model, but this is for demonstration purposes only.

So, we have the send_login_link method which we’ll make use of shortly to send the login link for a user. Before storing it to our database, we are actually hashing it using BCrypt which makes it more secure in case of a data breach. Along with this also add the gem ‘bcrypt’ to your Gemfile.

Once we generate the login token, send it to the user in an email using ActionMailer UserMailer. Setting up the mailing functionality is skipped in this tutorial since there are many good tutorials out there that explain how to do them according to your email provider. Just make sure you include the link argument that we pass to the UserMailer in `send_login_link` method in your email template that you send to the user.

The login_link is configured with a localhost url, but change it accordingly for your application. Also, the token_validity duration is set to 2 hours, but you are free to change it, obviously. Finally, add this line to the app/controllers/users_controller.rb create action right after the @user.save line:

...
if @user.save
  @user.send_login_link
...

Now that we have the necessary helper methods in place, let’s add the receiving route to handle the login link we send in the email.

Session Controller

Start by generating the controller for session.

$ rails g controller session auth

Update in the config/routes.rb file, changing get 'session/auth' to get '/auth/:user_id/:token', to: 'session#auth'. In the generated session_controller.rb file, add this code:

def auth
  token = params[:token].to_s
  user_id = params[:user_id] 
  user = User.find_by(id: user_id)

  if !user || !user.valid_token?
    redirect_to root_path, notice: 'It seems your link is invalid. Try requesting for a new login link'
  elsif user.login_token_expired?
    redirect_to root_path, notice: 'Your login link has been expired. Try requesting for a new login link.'
  else
    sign_in_user(user)
    redirect_to root_path, notice: 'You have been signed in!'
  end
end

Using the helper method, check whether the token is a valid or if it’s expired. If it’s not valid, redirect to the home page with the appropriate messages. There is a helper method we have used, sign_in_user, which we have to create. Open up app/controllers/application_controller.rb and add:

def sign_in_user(user)
  user.expire_token!
  session[:email] = user.email
end

def current_user
  User.find_by(email: session[:email])
end

We basically expire the token of the user and store the user’s email to the session. Also, we have added a helper method to retrieve the user from the session. The password-less functionality is ready, go ahead and try registering for a new user for testing the login functionality.

Login

As a final step, we’ll make use of our helper methods to do the user login. Start by adding these routes to the config/routes.rb file:

resources :session, only: [:new, :create]

and add the below code to the /app/controllers/session_controller.rb file:

def new
end

def create
  value = params[:value].to_s
  user = User.find_user_by(value)

  if !user
    redirect_to new_session_path, notice: "Uh oh! We couldn't find the username / email. Please try again."
  else
    user.send_login_link
    redirect_to root_path, notice: 'We have sent you the link to login to our app'
  end
end

We have just made use of the send_login_link to do the heavy lifting. The final piece of the app is the login form. Create the file app/views/session/new.html.erb and add the following form:

<%= form_tag "/session" do %>
  <label> Email / Username </label>
  <%= text_field_tag "value" %>
  <%= submit_tag "Login" %>
<% end %>

It’s just a simple form that does the job for us.

Conclusion

With that, we have arrived at the conclusion of the tutorial. Password-less login is really picking up these days and it provides our users with a less distracting and more convenient way to authenticate. Oh, and it also provides less overhead for everyone involved. I encourage you to try the password-less login approach for your application, at least as a supplemental method for login. That would be a start!

All the sample code used in this tutorial is available on Github, feel free to fork and play with it. I thank you for reading the tutorial and I hope it served your purposes.

Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account