Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
October 6, 2021 10:19 pm GMT

Password-less auth in Rails

One of the weakest points in your system can easily be end users credentials. Its easy to forget that most people dont enable 2FA, use a password manager or even have a reasonable length of password to begin with.

Instead of mandating that passwords should be a certain length and have 3 special characters, what if we just removed the need for passwords entirely?

In this tutorial Ill show you exactly how I have accomplished password-less accounts in Rails, using one-time passcodes and email.

How does it work?

The basic flow for logins is as follows:

  1. The user types their email address
  2. A one-time password is emailed to them
  3. Typing the OTP into the browser then logs them in

Nine passwordless-auth demo

For signups this differs slightly. When you submit an email that has no account, the page will reload and ask you for a first and last name, then submitting the form will create your account and send you a OTP to login with.

Signup flow

Benefits

No longer worrying about password security
Users cant have insecure or weak passwords, because they dont have a password to begin with! There is also no need for password resets, changing passwords and all the notifications and emails that go along with them.

Emails are verified as standard
No need to verify your email address, If a user gets the code and types it in, their email is verified.
For Nine we wanted to make sure potential customers emails are verified before creating orders and sending them to Stripe checkout.

Signup flow is much faster
Without needing to fill in a password and a password confirmation, the account creation form can be drastically simplified. This is much better UX, especially where commerce is concerned.

Why not use a third-party service?

There are plenty of third part auth services out there, magic.link being the one I have seen get the most attention.

For my personal experience, I never like relying on third parties for such a crucial part of my system.

I know, I know, rolling your own auth is a terrible idea and if I where building a password system I would use a library like Devise. If anyone has any security concerns or thoughts on my approach please reply and let me know, I would love to discuss it further!

Building it

For those interested, Ill show you all the relevant code, if you have further questions please ask in the comments.

Dependencies

To rely on secure OTPs we need a couple of dependencies in our Gemfile:

# One time passwordsgem "rotp"gem "base32"

app/models/user.rb

Your user should have the following database fields at a minimum.

create_table :users do |t|  t.string "email", null: false  t.string "first_name", null: false  t.string "last_name", null: false  t.string "auth_secret", null: false  t.datetime "created_at", null: false  t.datetime "updated_at", null: falseendadd_index(:users, :email, unique: true)

Next up, we need to add a few methods to the User model for generating and verifying OTPs.

class User < ApplicationRecord  before_create :generate_auth_secret  validates :email, email: true, presence: true  validates :first_name, :last_name, presence: true  def self.generate_auth_salt    ROTP::Base32.random(16)  end  def auth_code(salt)    totp(salt).now  end  def valid_auth_code?(salt, code)    # 5mins validity    totp(salt).verify(code, drift_behind: 300).present?  end  private  # This is used as a secret for this user to   # generate their OTPs, keep it private.  def generate_auth_secret    self.auth_secret = ROTP::Base32.random(16)  end  def totp(salt)    ROTP::TOTP.new(auth_secret + salt, issuer: "YourAppName")  endend

Note the salt is stored in a cookie and ensures the user can only login from the same web browser that they requested the login from. This means that if someone looked over their shoulder and got their auth code, they couldnt login on a different web browser.

UserLogin service

This service handles the business logic for dealing with requesting a code and verifying it was correct and it will keep our controllers tidy.

module UserLogin  module_function  # Called when a user first types their email address  # requesting to login or sign up.  def start_auth(params)    # Generate the salt for this login, it will later     # be stored in rails session.    salt = User.generate_auth_salt    user = User.find_by(email: params.fetch(:email).downcase.strip)    if user.nil?      # User is registering a new account      user = User.create!(params)    end    # Email the user their 6 digit code    AuthMailer.auth_code(user, user.auth_code(salt)).deliver_now    salt  end  # Called to check the code the user types  # in and make sure its valid.  def verify(email, auth_code, salt)    user = User.find_by(email: email)    if user.blank?      return UserLoginResponse.new(        "Oh dear, we could not find an account using that email.        Contact [email protected] if this issue persists."      )    end    unless user.valid_auth_code?(salt, auth_code)      return UserLoginResponse.new("That codes not right, better luck next time ")    end    UserLoginResponse.new(nil, user)  end  UserLoginResponse = Struct.new(:error, :user)end

Controllers and routes

Firstly we need an Authenticatable concern that will provide methods like current_user and user_signed_in?. You will also need to include Authenticatable inside your application_controller.rb file.

# app/controllers/concerns/authenticatable.rbmodule Authenticatable  extend ActiveSupport::Concern  def authenticate_user!    redirect_to auth_path unless current_user  end  def user_signed_in?    current_user.present?  end  def current_user    @current_user ||= lookup_user_by_cookie  end  def lookup_user_by_cookie    User.find(session[:user_id]) if session[:user_id]  endend

Add the follow to your config/routes.rb file.

resource :auth, only: %i[show create destroy], controller: :authresource :auth_verifications, only: %i[show create]

We need two controllers to make this work, AuthController handles requesting auth and logging out, whereas AuthVerificationsController handles checking the OTP was correct.

# app/controllers/auth_controller.rbclass AuthController < ApplicationController  skip_before_action :authenticate_user!, except: :destroy  def show; end  def create    session[:email] = params[:email]    session[:salt] = UserLogin.start_auth(params.permit(:email, :first_name, :last_name))    redirect_to auth_verifications_path  rescue ActiveRecord::RecordInvalid    # If the user creations fails (usually when first and last name are empty)    # we reload the form, and also display the first and last name fields.    @display_name_fields = true    render :show  end  def destroy    session.delete(:user_id)    redirect_to auth_path, notice: "You are signed out"  endend# app/controllers/auth_verifications_controller.rbclass AuthVerificationsController < ApplicationController  skip_before_action :authenticate_user!  def show    @email = session[:email]    render "auth/verify"  end  def create    @email = session[:email]    resp = UserLogin.verify(@email, params[:auth_code], session[:salt])    if resp.error      flash[:error] = resp.error      render "auth/verify"    else      session.delete(:email)      session.delete(:salt)      session[:user_id] = resp.user.id      redirect_to root_path, notice: "You are now signed in"    end  endend

Views

In these views I am using tailwind CSS, feel free to style them however you want.

<%# app/views/auth/show.html.erb %><p class="text-2xl text-gray-900 font-medium mb-3">  Whats your email?</p><%= form_with(url: auth_path, html: { data: { turbo: false } }) do |f| %>  <%= f.email_field :email, value: params[:email], placeholder: "[email protected]", class: "w-full rounded-md border-gray-300" %>  <% if @display_name_fields %>    <%= f.text_field :first_name, placeholder: "First name", class: "mt-3 w-full rounded-md border-gray-300" %>    <%= f.text_field :last_name, placeholder: "Last name", class: "mt-3 w-full rounded-md border-gray-300" %>  <% end %>  <%= f.submit "Continue", class: "w-full cursor-pointer flex relative justify-center items-center py-2 px-4 rounded-md font-medium leading-6 focus:outline-none transition-colors duration-150 ease-in-out border border-transparent text-white bg-pink-600 hover:bg-pink-500 focus:border-pink-300 focus:shadow-outline-gray mt-3" %>  <div class="mt-3 text-center text-gray-600 text-sm">By continuing you agree to our <a href="https://nine.shopping/terms" target="_blank" rel="noopener noreferrer" class="underline text-pink-500">Terms of Use</a></div><% end %>
<%# app/views/auth/verify.html.erb %><div class="leading-relaxed text-lg text-gray-600">  We just emailed you a six digit code, please enter it in the box below.</div><%= form_with(url: auth_verifications_path, html: { class: "mt-6" }) do |f| %>  <%= f.label :email, class: "flex items-center justify-between text-sm font-medium text-gray-700 mb-1" do %>    Email    <%= link_to "Change", auth_path, class: "text-gray-500 underline font-normal" %>  <% end %>  <%= f.email_field :email, value: @email, placeholder: "[email protected]", class: "w-full rounded-md border-gray-300 bg-gray-100", disabled: true %>  <%= f.label :email, class: "flex items-center justify-between text-sm font-medium text-gray-700 mb-1 mt-3" do %>    Auth code    <%= link_to "Re-send code", auth_path(email: @email), method: :post, class: "text-gray-500 underline font-normal" %>  <% end %>  <%= f.text_field :auth_code, class: "w-full rounded-md border-gray-300 text-2xl tracking-widest text-center", maxlength: 6 %>  <%= f.submit "Continue to your account", class: "w-full cursor-pointer flex relative justify-center items-center py-2 px-4 rounded-md font-medium leading-6 focus:outline-none transition-colors duration-150 ease-in-out border border-transparent text-white bg-pink-600 hover:bg-pink-500 focus:border-pink-300 focus:shadow-outline-gray mt-3" %><% end %>

Mailer

The final piece of the puzzle is hooking up the mailer to send out your OTPs.

class AuthMailer < ApplicationMailer  def auth_code(user, auth_code)    @user = user    @auth_code = auth_code    mail to: @user.email, subject: "Hey #{@user.first_name}, use this auth code to sign in"  endend
<h1>Hey <%= @user.first_name %>,</h1><p>Use the six digit code below to continue signing in to your account (this will expire in 5 minutes).</p><table class="attributes" width="100%" cellpadding="0" cellspacing="0">  <tr>    <td class="attributes_content">      <table width="100%" cellpadding="0" cellspacing="0">        <tr>          <td class="attributes_item">            <span style="display: block; font-size: 35px; font-weight: bold; letter-spacing: 10px; text-align: center;"><%= @auth_code %></span>          </td>        </tr>      </table>    </td>  </tr></table><p>If you didn't request this code you can safely ignore this email.</p>

Original Link: https://dev.to/phawk/password-less-auth-in-rails-4ah

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To