Authentication Setup

Setting up Devise with a Rails API for authenthication

A Quick Recap

In the previous article we setup the PostgreSQL database.

You are now ready for the next step: setting up the authentication. Feel free to refer to the part 3 branch of the GitHub repository if needed.

This is the 3rd installment of my Developing A Cross-Platform iOS & Android Social Media App series.

Overview

We are now going to be setting up the authenthication for our api. Our goal for part 3 is to implement authentication and prepare for creating our first endpoints.

So how do we achieve this? There is a ruby gem called devise. It handles everything auth related on a Ruby on Rails app, but it is not designed to be api only. Don't worry, we can still use it on our api with some adjustments. For simplicity, we will only be using devise to handle our User model and provide some helper methods for us.

Devise is packed with features so if at any point you want to tinker around with it, you already have it installed.

Installing Devise

First we will need to install the following libraries:

devise : will be used to handle our User model

bcrypt: used by devise to keep passwords salted, hashed

Add these lines to your Gemfile and run bundle install:

gem 'devise'
gem 'bcrypt', '~> 3.1.7'

Once all the gems are installed, we need to install devise. Keep in mind that devise will create a bunch of views for the frontend, we won't be using them. Run:

$ rails generate devise:install

Creating The Migration

Once devise is installed, we can generate the User model. Run this command so that devise can create the model and migrations for us:

$ rails generate devise User

The next step would be to migrate our database. Let's look at the migration it created under db/migrate/:

...
class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users  do |t|
      #...
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""
      #...
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at
      #...
      t.datetime :remember_created_at
      #...
      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    #...
  end
end

Let's walk through some of the lines:

class DeviseCreateUsers: This is the name of the migration and it is self documenting in that we can tell this migration creates users for us

create_table :users do |t|: This tells us that the migration will create the table users in our PostgreSQL database

t.string :email, null: false, default: "": This adds a column of the string type called email. It cannot be null due to null: false and will default to "" due to default: ""

t.string :encrypted_password, null: false, default: "": Similar to above, this created an encrypted_password column. This does not save the password in plain text. Devise will only put encrypted passwords in this column.

We will handle password resets on our own, so these two are not necessary, we can comment these out:

t.string :reset_password_token
t.datetime :reset_password_sent_at

This migration also adds indexes for email and reset_password_token as seen below:

add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true

You can remove the reset_password_token index but we will need the email index. This is because indexes make searching through a database much faster. If we ever want to find a user using the email, we won't have to search through each record until the right record is found. Instead it looks through an index that lists the emails and the user it belongs to.

Using UUIDs

We still have one more thing left before we can run the migration: implementing uuids to replace our ids. Instead of having our ids be 1, 2, 3, ..., our ids would look like: 8818f855-e567-4684-b63e-1f1a0952b70c.

Why is this important? UUIDs are globably unique and make it far easier to merge datasets. Overall, it is ideal for scalable applications, such as the one we are building. Your project may not be the next big social media app, but converting to UUID in the future would be far more of a pain than setting it up now.

Keep in mind that there are some cons such as it being harder to debug an incremeting id vs a UUID. There is also the performance cost. UUIDs would be 4 times the storage size of an incremeting id. PostgreSQL has native support for UUIDs but it still has a performance impact. At such a small scale, you won't notice the slow performance, but imagine when it is scaled up to a popular production app. For this series, we will stick with UUID, but don't default to UUID without looking at the pros and cons for your next app.

Luckily, UUID is very simple to enable in our migrations. To do this we would need to take a look at these lines:

def change
  create_table :users  do |t|

We would need to use the pgcrypto extension so we enable it using enable_extension 'pgcrypto' and changing the id column to uuids by adding id: :uuid. As a result the lines above should be updated to:

def change 
  enable_extension 'pgcrypto'
  create_table :users, id: :uuid  do |t|

Once this is complete, we can run $ rake db:migrate to run our migrations.

If The Migration Fails

If you accidentally ran the migrations, you can rollback the migrations using $ rake db:rollback. Keep in mind that you should almost never do this unless the migration hasn't been commited yet. This is because changing an existing migration that others have already applied to the database would cause issues. You especially do not want to do this when a migration is applied to production. We will cover the proper approach in the future.

Rolling back and applying migrations may not always work. There are commands you can run as a last resort, but only as a last resort. These should not be the go-to solution since this can cause issues if you aren't careful. Migrations may fail or not be applied when deploying to production or when other developers pickup your code.

One option is running $ rake db:reset to reset the entire database. Of course, don't do this on production as it will reset your database completely. Only use this as a last resort. (After a reset, you should import your most recent database backup if you have one)

In other cases you may need to manually delete the automatically generated schema.rb file by running $ rm db/schema.rb. Sometimes, Rails thinks the migration is applied already and doesn't update the schema.rb file.

Of course, these issues could be usually avoided all together by never editing a migration after you migrate and commit.

In the next part, we will check out the model that devise created!