How To Implement Simple Authentication Without Devise

Almost every web app requires a sign in form. In Ruby, the popular choice is the Devise gem for Rails.

Devise gives you a lot of functionality out of the box, but it can also be frustrating. It has a lot of “magic,” and is heavily coupled to various parts of Rails. If you’re requirements don’t exactly fit the gem – for example, anonymous users – you will be wading through documentation, how-to’s, and the source code of a few gems. That’s no fun, and it might be more trouble than it’s worth. And when that auth-related change request appears in a few months, will it be easy, or will you be facing a maintenance nightmare?

Authentication is actually pretty simple! If you have minimal requirements, you can roll your own authentication with less than 100 lines of code. In this article, I’m going to show you a tiny web app featuring sign in, sign out, and new user creation, with a sweet and simple manual implementation.

All the source code is available as a single-file here: simple_authentication.rb

Dependencies

The only real dependency is the BCrypt gem, which is used to hash the passwords. This is the gem that Devise and Rails already use by default – we will just be using it directly.

We will also be using Sinatra, but the code is very easy to translate to Rails. That’s because BCrypt is nicely decoupled from everything, unlike Devise.

Here is the Gemfile:

source 'https://rubygems.org'
gem 'sinatra', '~> 1.4'
gem 'bcrypt', '~> 3.1'

Hashing Passwords With BCrypt

The implementation revolves around two one-line methods:

def hash_password(password)
  BCrypt::Password.create(password).to_s
end

def test_password(password, hash)
  BCrypt::Password.new(hash) == password
end

The hash_password function takes a plain text password and encrypts it with a strong one-way algorithm. The idea is that it is impossible, or at least very difficult, to work out what the password was based on the hash. We never store the password, only the hash, so that the original password is protected in the event that someone breaks into our database.

The second function test_password is used to check whether a given password is the same as the one we have hashed earlier. We don’t have the original password anymore, so this works by hashing the given password and comparing both the hashes.

This is basic password security, and the bcrypt gem handles all the details for us.

The User Model

To keep this example ultra simple, the User model is just a plain old Struct, and the “database” is just an array.

User = Struct.new(:id, :username, :password_hash)
USERS = [
  User.new(1, 'bob', hash_password('the builder')),
  User.new(2, 'sally', hash_password('go round the sun')),
]

Each user has just three properties: an id, a username, and the hash of their password. If this was Rails, you would just create a User model with those three attributes.

Signing In

This is the code that runs when the user submits the sign in form:

post '/sign_in' do
  user = USERS.find { |u| u.username == params[:username] }
  if user && test_password(params[:password], user.password_hash)
    session.clear
    session[:user_id] = user.id
    redirect '/'
  else
    @error = 'Username or password was incorrect'
    erb :sign_in
  end
end

Security Note: On successful sign in, we clear the session before storing the user id. This is a security measure to prevent Session Fixation Attacks.

First we try and find the user with the given username. If that user exists and test_password says that the password is correct, then we have a successful sign in. When successful, we store the user’s id in the session and redirect to the home page. Otherwise, we just set an error message and render the sign in form again.

Accessing The Current User

Once the user is signed in, we need to know who they are on all future requests to app. Here is the code that returns the current user:

def current_user
  if session[:user_id]
     USERS.find { |u| u.id == session[:user_id] }
  else
    nil
  end
end

We check the session to see if it contains the user id that we set during sign in. If the user id exists, we find the user with that id. If this was Rails, you would do something like User.find(session[:user_id]).

You can also memoize the return value so that the current_user method doesn’t hit the database every time it is called.

In the template for the home page, we use current_user to say hello:

<p>Hello, <%= current_user.username %>.</p>

Signing Out

This is the code that runs when the user clicks the sign out button:

post '/sign_out' do
  session.clear
  redirect '/sign_in'
end

Since the user’s id is stored in the session, we can just clear the session to sign them out. Nice and simple!

Creating A New User

This is the code that runs after submitting the “Create New User” form:

post '/create_user' do
  USERS << User.new(
    USERS.size + 1,
    params[:username],
    hash_password(params[:password])
  )

  redirect '/'
end

In this little example app, we just create a new User object and append it to the array of all users. It would be the same in Rails except, instead of appending to an array, you would insert the new User object into the database.

Notice how the hash_password method is used. Remember that we never store the plain text password, only it’s hash.

The End

There you have it: a simple, working implementation of basic authentication. That wasn’t too hard, was it?

Try it out for yourself. The whole app is available as a single file here: simple_authentication.rb

Got questions? Comments? Milk?

Shoot an email to tom@rubypigeon.com or hit me up on Twitter (@tom_dalling).

Next up: 4 Ways To Avoid Monkey Patching →

Join The Pigeonhole

Don't miss the next post! Subscribe to Ruby Pigeon mailing list and get the next post sent straight to your inbox.