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!

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.

  • Brendon Murphy

    Thanks for sharing this flow, it’s a neat idea as an alternative to traditional passwords!

    I think there’s a couple pretty easy changes that could make this scheme more secure. First, since the token is effectively a password (albeit short lived and one time) it would be good to store more securely in the database. If you run it through bcrypt or something similar when saving, if read access to the token column was compromised, your security is still likely intact. By storing them in plaintext, if an attacker can read that column (for example, they compromise a fresh backup of your database), they could potentially gain access to a user account.

    Secondly, rather than finding the user by the token, you could route to auth like /auth/:user_id/:token. Then you would find the user by user_id, and do a secure compare of the token using bcrypt, so as to not be subject to timing attacks. Devise, for instance, used to ship with a token authentication string mechanism, but dropped it due to the possibility of timing attacks against a btree index on the token column. By finding via id, you are no longer subject to a timing attack. The code would probably only be a handful of lines more, and I think would still be pretty clear and concise.

    Thanks again for sharing some outside the box thinking.

    • Jordano Moscoso

      I think two factor authentication is the best choice today.

    • @xternal:disqus Those are some nice points. Thanks for sharing. I will definitely update this tutorial with your input in a week or so +1

    • ggsp

      Really great comments here. Thanks for sharing Brendon!

  • William Hitoshi Kurosawa

    Yep, cool tutorial to show alternatives to keep track of passwords. (even with security concerns pointed by Brendon, it’s really nice to think on alternatives)

    Just let me point some typos, maybe could help anyone having trouble.

    Method send_login_link, the argument being passed should be `template` instead of `’emplate`
    `UserMailer.send(’emplate).deliver_now`

    Method login_link should pass a key/value:
    `”http://localhost:3000/auth?token=#{self.login_token}”` instead of `”http://localhost:3000/auth?#{self.login_token}”`

    Method login_token_expired? should be reversed logic:
    “`
    Time.now.utc > (self.token_generated_at + token_validity)
    “`
    instead of
    “`
    (self.token_generated_at + token_validity) > Time.now.utc
    “`

    And method auth in SessionController should be calling method `sign_in_user` instead of `signin_user`

    • ggsp

      Think I got em all…thanks for catching that!

    • Thank you William. The tutorial is now updated.

  • Great information.

Recommended
Sponsors
Get the latest in Ruby, once a week, for free.