DEV Community

Prasanna Natarajan
Prasanna Natarajan

Posted on • Originally published at

Rails Authentication From Scratch. Going Beyond Railscasts

If you have to implement authentication in your rails app, Devise is your safest option. Rolling your own is shooting yourself in the foot.

But using Devise didn't feel like coding to me. It's like setting up your new mac by reading instructional blog posts around the net. Devise has great documentation and has all of your questions covered. You just follow them and you get industry level security.

But it would be good coding practice if we can understand how Devise, and authentication in general works.

So I implemented it from scratch following the famous Ryan Bates tutorial. But the actual motivation came from Justin Searls, who in his recent talk "Selfish Programmer" said he himself doesn't understand Devise and so implemented authentication from scratch for one of his side project. He implemented the usual workflows all by himself - the signup, sign in, forgot password, reset password etc - which helped him "keep all of his app's code within his head". (Which is a state you too should be in during the entire life of a project you are involved in.)

I did the same thing. But after the main workflows, I started implementing other features similar to the way Devise had done them. I just cloned their repo and searched it for how a feature was implemented. For each of the feature that Devise supports, they take care of all possible edgecases. I could care less. So I took only the core of the feature and coded it.

The features I implemented are:

  • user registration
  • authentication by email and password (signin and signout)
  • remember and redirect to an auth-required page that the user tried to visit while logged-out, and then logged in
  • user confirmation (only confirmed users can do certain/all actions)
  • forgot password and password-reset

In the rest of the section, I'll explain how I implemented these. For missing pieces of the code here, you can find them in the actual repo:

The User database design

Here's the users migration file showing all the fields, indices and the datatypes.

class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name

      t.string :email, null: false
      t.string :password_digest

      t.string :remember_token

      t.string :password_reset_token
      t.datetime :password_reset_sent_at

      t.string :confirmation_token
      t.string :unconfirmed_email
      t.datetime :confirmation_sent_at
      t.datetime :confirmed_at


    add_index :users, :email, unique: true
    add_index :users, :remember_token, unique: true

    # I like to use empty lines to group related code
    add_index :users, :password_reset_token, unique: true

    add_index :users, :confirmation_token, unique: true
    add_index :users, :unconfirmed_email, unique: true

Code structure

All of devise's controllers inherit from DeviseController. I'd like my authentication functionalities to inherit from a PrasDeviseController.

(Not just for vanity reasons. Earlier I had user creation (signup) happen in UsersController, which made it hard to pull all common code in an ancenstral controller. That's when I realized Devise has a special thing called registrations which is where user creation happens. This allows us to make UsersController do non-authentication stuff while pulling out the user creation code to the authentication related code. Ok, the naming is still vanity.)

The PrasDevise controller hosts all the common methods that's used by the other auth-related code. As an example, this is a great place to put your recaptcha code if you are ever going to use them in any of your auth forms. I put them everywhere - signup, login, password-reset etc just to annoy the user (who's just me). Here are the 2 methods used for recaptcha:

Since the controllers are scoped, it'd be nice if the urls are scoped too. So here's how the routes.rb file looks like:

  scope module: :pras_devise do
    resources :registrations, only: [:new, :create]
    resources :confirmations, only: [:new, :show]
    resources :sessions, only: [:new, :create, :destroy]
    resources :password_resets, only: [:new, :edit, :create, :update]

These controller files are all present in app/controllers/pras_devise/ folder.

The User Registration workflow

I'd like to allow all authenticated operations to be performed by confirmed users only. A confirmed user is one who had clicked a special link sent to their email that they claimed is theirs by signing up.

In the create action, we lookup/initialize the user only by the unconfirmed_email field and not by the email field. Once the user confirms, we'll remove the email from unconfirmed field.

    # in pras_devise/registrations_controller.rb
    def create
      @user = User.find_or_initialize_by(unconfirmed_email: user_params[:email])
      @user.attributes = user_params
        @user.generate_token_and_send_instructions!(token_type: :confirmation)
        redirect_to root_url, notice: "Check your email with subject 'Confirmation instructions'"
        render :new

    private def user_params
        .permit(:name, :email, :password, :password_confirmation)

(Notice the 2nd line in the create action. I found it a nice way to assign a hash-like datastructure to all attributes of an activerecord object.)

The User Confirmation workflow

The user.generate_token_and_send_instructions! method just generates a unique confirmation_token and sends an email to that user with a link containing that token.

  # in models/user.rb

  # token_type is:
  # confirmation for confirmation_token,
  # password_reset for password_reset_token
  # etc.
  def generate_token_and_send_instructions!(token_type:)
    self[:"#{token_type}_sent_at"] =
    UserMailer.with(user: self).send(:"email_#{token_type}").deliver_later

The mailer method looks like so:

  # in mailers/user_mailer.rb
  def email_confirmation
    @user = params[:user]
    @email = @user.unconfirmed_email
    @token = @user.confirmation_token

    mail to: @email, subject: "Confirmation instructions"

And the email itself looks like this:

<p>Welcome <%= @email %>!</p>

<p>You can confirm your account email through the link below:</p>

<p><%= link_to 'Confirm my account', confirmation_url(@user, confirmation_token: @token) %></p>

The link looks something like this: where 111 is the user's id which is irrelevant here. We only need it because we are defining the related controller action in a restful manner.

In confirmations_ocntroller#show action we lookup the user by the confirmation_token param. If the token isn't expired yet, then we confirm them by marking the unconfirmed_email as nil and saving the record.

# confirmation_controller.rb
    def show
      user = User.find_by(confirmation_token: params[:confirmation_token])

      if user.confirmation_token_expired?
        redirect_to new_registration_path, alert: "Confirmation token has expired. Signup again." and return

      if user, user.unconfirmed_email = user.unconfirmed_email, nil
        user.confirmed_at =
        redirect_to root_url, notice: "You are confirmed! You can now login."
        redirect_to root_url, alert: "No user found for this token"

Note that if the token expired, we are redirecting to the signup page. If the user now tries to signup with the same email, it will still work because there, in registrations_controller#create we use User.find_or_initialize_by rather than every time someone attempts to signup.

The Sign-In / Sign-Out workflow

This is very straightforward.

  • find the user from db based on the email incoming from the sign-in form
  • try to authenticate the user with the incoming password
  • If the authentication succeeds, create a unique remember_token, encrypt and save it in a cookie.
    def create
      if @user&.authenticate(params[:password])
        redirect_to after_sign_in_path_for(:user), notice: "Logged in!"
      else = "Email or password is invalid"
        render :new

    private def login!
      unless @user.remember_token
      if params[:remember_me]
        cookies.encrypted.permanent[:remember_token] = @user.remember_token
        cookies.encrypted[:remember_token] = @user.remember_token

If the user checked the Remember me checkbox in the sign in form, then we create a permanent cookie, otherwise it's just a normal one.

If the authentication succeeds, we redirect the user to the page they previously attempted to visit unsuccessfully because they were un-authenticated at the time. This part of the code is taken straight from Devise. It's all in the parent controller PrasDeviseController.

Let's see how it's implemented in detail...

Redirect to specific page after sign in

To do this, first you need to save each page the user visits in a session (a fancy cookie). But not all page should be saved. Devise saves only requests that satisfy all of these conditions:

  • the request should be a GET request. Anything else sounds dangerous and is not simple to implement either. (Here's an interesting stack overflow discussion about this.)
  • the request should not be an ajax request
  • the request should be for an action that doesn't come from any PrasDevise controller. ie, it shouldn't be for pages like sign-in, sign-up, forgot-password forms etc. Doesn't make sense
  • the request format should be of html only

These kind of requests are then stored in a session cookie with the key :user_return_to. Once the user successfull logs in, then the sessions_controller#create action redirects them to the correct

So, here's a before_action callback that's called on every request to the app.

# in pras_devise_controller.rb

    before_action :store_user_location!, if: :storable_location?

    private def storable_location?
      request.get? &&
        is_navigational_format? &&
        !is_a?(PrasDevise::PrasDeviseController) &&

    private def is_navigational_format?
      ["*/*", :html].include?(request_format)

    private def request_format
      @request_format ||= request.format.try(:ref)

    private def store_user_location!
      # :user is the scope we are authenticating
      #store_location_for(:user, request.fullpath)
      path = extract_path_from_location(request.fullpath)
      session[:user_return_to] = path if path

    private def parse_uri(location)
      location && URI.parse(location)
    rescue URI::InvalidURIError

    private def extract_path_from_location(location)
      uri = parse_uri(location)
      if uri 
        path = remove_domain_from_uri(uri)
        path = add_fragment_back_to_path(uri, path)

    private def remove_domain_from_uri(uri)
      [uri.path.sub(/\A\/+/, '/'), uri.query].compact.join('?')

    private def add_fragment_back_to_path(uri, path)
      [path, uri.fragment].compact.join('#')

    private def after_sign_in_path_for(resource_or_scope)
      if is_navigational_format?
        session.delete(:user_return_to) || root_url
        session[:user_return_to] || root_url

(It all came from Devise.)

The Password Reset workflow

For password reset, you need 4 actions.

  • new shows the form where the user would input their email.
  • It would then be submitted to create where the app would generate a password_reset_token and email it to the incoming email.
  • The user would then click the link in the email which would take him to a password edit page where the user is found by the password_reset_token from the link.
  • Once the user fills the form with the new password and submits, it will go to the update action which saves the new password in the database.

Here's the controller code:

# password_resets_controller.rb

    def new

    def create
      user = User.find_by(email: params[:email])
      user&.generate_token_and_send_instructions!(token_type: :password_reset)
      redirect_to root_url, notice: "If you had registered, you'd receive password reset email shortly"

    def edit
      redirect_to root_url, alert: "Cannot find user!" unless @user

    def update
      if ( - @user.password_reset_sent_at) > 2.hours
        redirect_to new_password_reset_path, alert: "Password reset has expired!"
      elsif @user.update(password_update_params)
        redirect_to root_url, notice: "Password has been reset!"
        render :edit

    private def set_user
      @user = User.find_by(password_reset_token: params[:id])

    private def password_update_params
        .permit(:password, :password_confirmation)

Note that when the update form is submitted, we make sure the password_reset email was sent very recently. We don't want users to abuse this functionality.

The email_password_reset.html.erb template looks like this:

To reset your password, click the URL below.

<%= link_to edit_password_reset_url(@user.password_reset_token), edit_password_reset_url(@user.password_reset_token) %>

If you did not request your password to be reset, just ignore this email and your password will continue to stay the same.


It's great to spend time looking under the hood of any library. With the help of example codes and test cases in the Devise repo, I was able to put pieces together and find out how some of the main functionalities work. I also came across many samples of succint and beautiful code, especially their test cases. Just by using minitest and mocha they've written short but easily readable test cases.

Nothing is mysterious if you take the time to explore it with curiosity.

Top comments (0)