Digest: Authentication with Ruby on Rails
Posted on by Herbert Ilhan Tanujaya
Nowadays, nearly all websites have an authentication system in place, using usernames and passwords. In this article, we will create a simple authentication in Ruby on Rails, a popular web application framework. This article is mainly aimed at those who are just getting started with their first web application (but have some knowledge of Rails).
When I first learned Rails and web development in general, I wanted to create a website for my organization. One requirement for the website was to have an authentication system, so I searched for Ruby gems that could help with this. One of the most popular gems is devise. However, this gem is not recommended for beginners to Rails as mentioned in the README!
Based on my experiences, this article will walk through the steps needed to create a sufficiently secure authentication system in Ruby on Rails 5.0.1 on Ruby 2.4.0.
What to store in database?
It is totally not advisable to store plain passwords in the database. For one, developers can see the password you store, which is already a security breach. An even more important reason, though, is to limit the damage when your database is compromised. Imagine if you store plain passwords in the database and your database is stolen by hackers - now the hackers are able to compromise all accounts in your system easily. Now imagine if you store them in a secure format such that even if the hackers steal your database, they still can’t get the passwords easily - less damage would be done that way.
To achieve this, we will hash the passwords in the database. Basically, what this means is that we will use a hash function that takes your passwords as an input and gives out a (seemingly) random string of a certain length. There are some properties that make certain functions suitable as a hash function: - it has finite output possibilites (since it has a fixed length); however, it is almost impossible to find two inputs that hash to the same output - computing the hash is quick and deterministic (meaning that hashing the same input always produces the same output) - given a hash, it is nearly impossible to find an input that hashes into it - a small change to the input makes a big change in the output
For our purposes, we will be using BCrypt that is conviniently bundled already in Ruby on Rails.
There is another problem though - if we only store the password hash, an attacker can compute as many hashes as they can, and store it in some table. When the database is compromised, the attacker can just use this table to lookup inputs that hash to these passwords! This attack is called rainbow tables.
Hence, we need to “upgrade” our hashing system by including password salts. Essentially, this means our hashes depend not only on our initial passwords, but also on the randomly generated salts. Each salt is associated with a user in our system and every user should have different salts, so that precomputing the hashes would take much more time (since the attacker also needs to use various salts as well).
Hence, in our database, we will store three things: 1. Username - string, not null, must be unique 2. Password hash - string, not null 3. Password salt - string, not null
So much about security. Let’s get to work!
Creating our application
Let’s create our app! Assuming you have installed Rails
(check this out if you
haven’t), in your project root, run
rails new simple-auth (here
is the name of my app). After the installation is done, enter your app folder.
Now, let’s generate the models we need for the application. To do so, run:
bin/rails g model User username:string password_hash:string password_salt:string
Check the migration file generated (in
db/migrate folder). Let’s add not null
and unique constraints on the database by changing the migration file into:
class CreateUsers < ActiveRecord::Migration[5.0] def change create_table :users do |t| t.string :username, null: false, unique: true t.string :password_hash, null: false t.string :password_salt, null: false t.timestamps end end end
bin/rails db:migrate to migrate the database.
Next, enable the BCrypt gem by uncommenting the corresponding line in the
# gem 'bcrypt', '~> 3.1.7'. Run
bundle install afterwards
Now, in your User model, add these lines:
class User < ApplicationRecord attr_accessor :password validates :username, presence: true, uniqueness: true validates :password, presence: true, on: :create before_validation(on: :create) do encrypt_password end def authenticate(password) password_hash == BCrypt::Engine.hash_secret(password, password_salt) end private def encrypt_password self.password_salt = BCrypt::Engine.generate_salt self.password_hash = BCrypt::Engine.hash_secret(password, password_salt) end end
Here’s some explanation:
attr_accessor :password adds a “password” field in your User model that is
not saved into the database.
- The next two lines validate both username and password, by making sure they
exist. Username is also ensured to be unique. Now you might realize that the
username is already guaranteed in the database to be not null and unique; why
bother doing it again in the model? Actually, it is enough to have it in either
the model or the database (assuming the database only interacts with this app).
I’m just going to including it in both.
before_validation block makes sure that the password is encrypted
authenticate method provides a method in the user to check if the given
password (as argument) is the user’s correct password.
encrypt_password method generates the salt and the hash. It is set
private because there is no need to expose this method.
Now that we’ve done this, let’s open the console by using
Let’s create a user! For example, I’ll create a user named
NUSh4ck3r5. Run in irb:
u = User.create(username: 'donjar', password: 'NUSh4ck3r5')
The user should now be saved into the database. If you run
User.all it should
show your user, along with its hash and salt. Here’s what happened in my
#<ActiveRecord::Relation [#<User id: 1, username: "donjar", password_hash: "$2a$10$xb9MMJaEm7aP.lKdIPG0aeH/etrXqE4A/ObLri/8IMT...", password_salt: "$2a$10$xb9MMJaEm7aP.lKdIPG0ae", created_at: "2017-02-16 15:03:23", updated_at: "2017-02-16 15:03:23">]>
This can be different from the user in your computer - it’s okay! In fact, this shows that the salt is randomly generated, and with the same password yet a different salt, the hash will be very different.
Now we can test authenticating our user:
> u.authenticate('NUSh4ck3r5') => true > u.authenticate('NUShackers') => false > u.authenticate('nush4ck3r5') => false
So this means our user system is working. Yay!
For a simple authentication system that is only working in the model, this is sufficient. However, if you are going to create a full application with view and controller, you also need to add the login and logout functions, which need more than this. There are many tutorials online - we might even add one in the future!