Sinatra Authentication

22 February 2015

I thought I’d take a little break from blogging about iOS app development to write about how I added authentication to a Ruby web application recently. I’m a big fan of the Sinatra micro framework for when the full-stack Ruby on Rails framework is too heavyweight. I like Sinatra because it makes writing simple (or fairly simple) web applications really easy, whilst allowing you to make your own choices about the model and view layers.

As you might imagine given its simplicity, Sinatra has no built-in support for performing authentication. When I needed to add it to an application I was working on the other day I had a look to see what existing solutions were out there. It’s easy to implement HTTP basic authentication using Sinatra, but I wanted to use my own sign-in page that matched the styling of the rest of the application, rather than rely on the ugly browser-provided authentication dialogue.

Warden is a deservedly popular and powerful Ruby authentication framework and there’s a nice Sinatra example application, but looking at the code it looked a bit heavyweight and not quite straightforward enough for my needs, with its support for multiple authentication strategies. Therefore I decided to roll my own solution.

Of course, the first rule of security is never to roll your own solution, because inevitably someone out there has already done what you’re trying to do and in a better and more secure way. However, I’m not talking about setting out to implement my own cryptography algorithm or anything crazy like that. What I did was implement the username and password checking and the required routing logic, whilst relying on industry standard encryption using bcrypt.

I’ve extracted my solution into a minimal example application and the source code is available on GitHub for you to follow along. I’d also appreciate it if you let me know of any flaws you discover in my approach (pull requests welcome). Note that this blog post assumes a basic familiarity with Sinatra. If you don’t have that then I recommend reading the getting started guide.

Most of the action takes place in app.rb. Routes within the application that are to be secured should have authenticate! as their first line, as for the /protected route in the example code:

get '/protected/?' do
  authenticate!
  erb :protected, locals: { title: 'Protected Page' }
end

The authenticate! method is declared as a helper method and implemented in the Authentication module in lib/authentication.rb. Here’s the code:

module Authentication
  def authenticate!
    unless session[:user]
      session[:original_request] = request.path_info
      redirect '/signin'
    end
  end
end

As you can see, there’s not much to it. If there’s no user object in the HTTP session then the incoming URL is stashed in the session under the :original_request key and an HTTP redirect is performed to the sign-in page. The /signin route just renders the sign-in page:

get '/signin/?' do
  erb :signin, locals: { title: 'Sign In' }
end

The sign-in form

That sign-in form submits an HTTP POST request to the /signin route, the code for which is shown below:

post '/signin/?' do
  if user = User.authenticate(params)
    session[:user] = user
    redirect_to_original_request
  else
    flash[:notice] = 'You could not be signed in. Did you enter the correct username and password?'
    redirect '/signin'
  end
end

The first line of this route calls the User class’s authenticate class method, passing the params hash. This is an object Sinatra automatically provides to routes that contains all the parameters for the current HTTP request. In this case it contains the username and password from the submitted form.

The authenticate class method acts as a factory method in that it returns an instance of the User class if the authentication succeeded, otherwise it returns nil. If nil is returned then the assignment to the user local variable fails. This causes the else clause to be entered, which displays a message and performs a redirect back to the /signin route shown above so the sign-in form is displayed again. The current implementation does not limit the number of sign-in attempts, although this wouldn’t be too difficult to add.

The actual authentication logic is contained within the User class in /lib/user.rb. The implementation is shown below:

require 'bcrypt'
require 'yaml'

class User
  include BCrypt
  attr_reader :name

  def self.authenticate(params = {})
    return nil if params[:username].blank? || params[:password].blank?

    @@credentials ||= YAML.load_file(File.join(__dir__, '../credentials.yml'))
    username = params[:username].downcase
    return nil if username != @@credentials['username']

    password_hash = Password.new(@@credentials['password_hash'])
    User.new(username) if password_hash == params[:password] # The password param gets hashed for us by ==
  end

  def initialize(username)
    @name = username.capitalize
  end
end

I need to make a couple of important points before discussing this code. The first is that to keep the example application simple I’m storing the username and hashed password in a YAML configuration file named credentials.yml. Whilst this is certainly one approach, it doesn’t scale to more than one user! Therefore in reality you’d want to store this data in a database table. For my real application I used MySQL and created a users table and a Ruby class that encapsulates access to it.

The second point is that this blog post is not going to cover how users register to use the application and therefore how user accounts get created. With those caveats out the way, on to the code.

The User class mixes in the BCrypt module from the bcrypt RubyGem. Our class has one read-only property, which is the name of the signed in user. This is just a convenience so it can be easily displayed on the screen. If the entered username or password (as provided by the params hash) are blank strings, the authenticate class method immediately fails and returns nil. It also fails if the username isn’t known to the application. Note that when using a database at this point you’d perform a SQL query with a WHERE clause to fetch the user record by the entered username.

The Password.new method creates a Password object from the stored password hash. The Password object comes from the included BCrypt module. This module also overrides the double equals (==) method within the Password class. Another instance of Password is created from the entered password and the two instances are compared for equality. If they are equal then the passwords match and a User instance is returned from the method. Otherwise, nil is returned and again authentication fails.

Back over in app.rb, if authentication succeeded then the returned User object is stored in the session and the redirect_to_original_request helper method is called. The code for this method is shown below:

def redirect_to_original_request
  user = session[:user]
  flash[:notice] = "Welcome back #{user.name}."
  original_request = session[:original_request]
  session[:original_request] = nil
  redirect original_request
end  

The User object is retrieved from the session and a message displayed welcoming the user back. The original URL is also retrieved and a redirect performed to that URL. This is so the user is taken back to the page they originally requested before their request was intercepted by the sign-in process.

The only thing left to cover is the /signout route, which looks like this:

get '/signout' do
  session[:user] = nil
  flash[:notice] = 'You have been signed out.'
  redirect '/'
end

The User object is removed from the session because there is no current user as they’ve signed out. You may recall that not having a User object in the session causes the Authentication module to intercept any requests for pages matching routes protected by a call to the authenticate! method and the whole cycle starts again.