Search
⌘K
    to navigateEnterto select Escto close

    Authentication

    Bcrypt gem

    Authentication involves validating password provided by a user. For security reasons passwords should be stored in an encrypted form rather than in the plain text. In this way if the database is ever compromised, hacker would not be able to get the actual password of the users since the database is keeping only the encrypted password.

    The bcrypt gem allows us to get hash of the password in a secure manner.

    Let's add this gem to our Gemfile:

    1gem 'bcrypt', '~> 3.1.13'
    1bundle install

    Adding credentials fields to User model

    Now let's add two fields to User model. First field will store email and the second field will store password.

    We will also put a unique index on column "email". When an index is declared unique, multiple table rows with the same value are not allowed. Note that two null values are not considered equal:

    1bundle exec rails g migration AddEmailAndPasswordDigestToUser

    Open db/migrate/add_email_and_password_digest_to_user.rb file and add following lines:

    1class AddEmailAndPasswordDigestToUser < ActiveRecord::Migration[6.1]
    2  def change
    3    add_column :users, :email, :string, null: false, index: { unique: true }
    4    add_column :users, :password_digest, :string, null: false
    5  end
    6end

    The Active Record uniqueness validation does not guarantee uniqueness at the database level. Here’s a scenario that explains why:

    1. Sam signs up for the sample app, with address sam@example.com.
    2. Sam accidentally clicks on “Submit” twice, sending two requests in quick succession.

    The following sequence occurs: request 1 creates a user in memory that passes validation, request 2 does the same, request 1’s user gets saved, request 2’s user gets saved.

    Result: two user records with the exact same email address, despite the uniqueness validation If the above sequence seems implausible, believe me, it isn’t. It can happen on any Rails website with significant traffic.

    Luckily, the solution is straightforward to implement: we need to enforce uniqueness at the database level as well as at the model level. Our method is to create a database index on the email column, and then require that the index be unique.

    Now let's run the migration:

    1bundle exec rails db:migrate

    Running the above migration would raise the following error:

    1SQLite3::ConstraintException: NOT NULL constraint failed: users.email

    This is because in our database we already have users who do not have any email. In this migration we are telling Rails to put a unique constraint on the email column of users table. The database can't do that as long as we have records with null email.

    So let's remove all user's records. Time to fire up the Rails console:

    1$ bundle exec rails console
    2>> User.delete_all

    Another reason for this error could be fixtures files added by the Rails model generation command. Check if there are any fixtures files inside test/fixtures. If yes then remove them by running the following command:

    1rm -rf test/fixtures/*

    Now run the migration one more time:

    1bundle exec rails db:migrate

    Securing password

    As we discussed earlier password should not be stored in the plain text in the database. Rails provides has_secure_password to conveniently store password in an encrypted manner. Rails needs column password_digest in the User model to do its job.

    Add following line to User model:

    1class User < ApplicationRecord
    2  has_many :assigned_tasks, foreign_key: :assigned_user_id, class_name: "Task"
    3  has_secure_password
    4
    5  validates :name, presence: true, length: { maximum: 35 }
    6end

    has_secure_password line adds some convenience methods to the User class. These methods help in storing password in an encrypted form and also authenticate the plain text password with the stored encrypted password. From the Rails source code we can see that has_secure_password adds methods like password=, password_confirmation and authenticate_password.

    Adding credential validations

    Let's add validations to our newly added fields, email and password:

    1class User < ApplicationRecord
    2  has_many :assigned_tasks, foreign_key: :assigned_user_id, class_name: "Task"
    3  has_secure_password
    4
    5  validates :name, presence: true, length: { maximum: 35 }
    6  validates :password, length: { minimum: 6 }, if: -> { password.present? }
    7  validates :password_confirmation, presence: true, on: :create
    8end

    has_secure_password auto-magically adds validations for presence of password on create and confirmation of password (by default it's password_confirmation attribute).

    The confirmation validation creates a virtual attribute whose name is the name of the field that has to be confirmed with and "_confirmation" appended to it. Here it'd become "password_confirmation". We need to ensure the presence of confirmation field during creation.

    We have added an if condition which will only validate the password length if the password entered in not nil.

    Don't let this confuse you. Rails will validate the password for all measures such as presence and confirmation except for the length validation if the password is nil. Hence if no password is entered, an error will be thrown.

    Note that, here we are validating the presence of password_confirmation field only on Active Record create method. The reason is that, currently we need the password confirmation only when signing up for first time.

    Before adding a validation for email, let's declare a constant for the maximum length of an email. Add the following line in config/initializers/constants.rb file:

    1module Constants
    2  is_sqlite_db = ActiveRecord::Base.connection_db_config.configuration_hash[:adapter] == "sqlite3"
    3  DB_REGEX_OPERATOR = is_sqlite_db ? "REGEXP" : "~*"
    4  MAX_TASK_TITLE_LENGTH = 125
    5  MAX_NAME_LENGTH = 255
    6  MAX_EMAIL_LENGTH = 255
    7end

    Now add the following changes to the user model to validate the email field:

    1class User < ApplicationRecord
    2  VALID_EMAIL_REGEX = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i.freeze
    3
    4  has_many :assigned_tasks, foreign_key: :assigned_user_id, class_name: "Task"
    5  has_secure_password
    6
    7  validates :name, presence: true, length: { maximum: 35 }
    8  validates :email, presence: true,
    9                    uniqueness: true,
    10                    length: { maximum: Constants::MAX_EMAIL_LENGTH },
    11                    format: { with: VALID_EMAIL_REGEX }
    12  validates :password, length: { minimum: 6 }, if: -> { password.present? }
    13  validates :password_confirmation, presence: true, on: :create
    14
    15  before_save :to_lowercase
    16
    17  private
    18
    19    def to_lowercase
    20      email.downcase!
    21    end
    22end

    The format validator ensures the field has specified format, here, VALID_EMAIL_REGEX. The before_save callback is called every time an object is saved. Before saving, the to_lowercase method makes all characters of email in lowercase.

    The freeze method in Ruby

    At this point, you might be wondering why we are using the freeze method on VALID_EMAIL_REGEX constant. To understand this we need to understand the nature of constants in Ruby.

    Contrary to what the name suggests, constants are mutable in Ruby. Writing a variable name in all caps doesn't make it a constant in the way you may expect. Thus altering them will not throw a RuntimeError exception, but only a warning. Let's test this out. Open the Rails console by running the following command:

    1bundle exec rails c

    Now run the following commands from the Rails console:

    1irb(main):001:0> CONSTANT_STRING = "This is a mutable "
    2irb(main):002:0> CONSTANT_STRING << "constant"
    3irb(main):003:0> CONSTANT_STRING
    4=> "This is a mutable constant"

    As you can see this doesn't throw an error like you would expect. This is where the freeze method comes in. The freeze method prevents further modifications on the object it is called on. In other words, it makes the constant immutable.

    Let' see this in action. Reload and type the following commands in your console:

    1irb(main):001:0> CONSTANT_STRING = "This is an immutable".freeze
    2irb(main):002:0> CONSTANT_STRING << "constant"

    Now you should see the following error in your console

    1(irb):4:in `<main>': can't modify frozen String: "This is an immutable" (FrozenError)

    Thus the freeze method allows us to make truly immutable constants in Ruby.

    What is the frozen_string_literal comment?

    If you have correctly setup Rubocop correctly as we had mentioned in early chapters, then you may notice the comment #frozen_string_literal: true being added to the top of any Ruby file automatically when you run rubocop on it.

    This comment essentially tells Ruby that all string literals in the file are implicitly frozen, as if freeze had been called on each of them. This avoids us the hassle of making sure to call the method freeze on any string literal we define and also makes them behave like expected.

    So we can remove the freeze method from VALID_EMAIL_REGEX if we have configured Rubocop to add the frozen_string_literal comment on top of all Ruby files. But in the upcoming chapters, we will be chaining the .freeze method explicitly so as to reinforce the use case of freeze method.

    Note: The frozen_string_literal comment only affects the behaviour of string literals. Other collections like arrays, hash literals, etc won't be affected by the comment.

    Now let's commit these changes:

    1git add -A
    2git commit -m "Added has_secure_password and validations to User model"
    Previous
    Next