Simple API Authentication in Sinatra

ruby

Sinatra is a great choice for building out simple JSON APIs. Combined with Ember.js, it is now my preferred stack to build out web apps on, as opposed to using Ruby on Rails. However, there aren’t as many existing, ‘standard’ libraries for Sinatra, like Rails' devise. So, how shall we secure a Sinatra API?

Note that I’m not a security expert and this is probably not best practice. Please treat what I’m doing here as a learning experiment.

First, a word on security: if every request between the application and the outside world is going to be routed over a secure connection using TLS (formerly SSL), then we can simplify our authentication design significantly. For many APIs using TLS, just using the bog-standard HTTP Basic Authentication is totally acceptable. Note that HTTP Basic Auth just sends a “username” and “password” in the HTTP headers without any encryption, on every request.

So, let’s assume that we’re going to use TLS when we deploy the application. We could just use HTTP Basic Auth - this is well documented. But, there are certain oddities of Basic Auth, including the way that browsers often interact with it. Plus, if we use basic auth, the user has to send their password to the server with every request, and we have to do a password comparison on every request. This isn’t fun, especially if we’re using a computationally expensive password hashing algorithm, as we should.

Instead, we will implement a basic variation of token based authentication.

Here’s the procedure:

There are several major advantages here. One, the user doesn’t repeatedly send their password - if a token gets stolen or packet sniffed, it is a concern, but we can just expire the token. We can also change our token scheme easily, issuing multiple tokens to the same user, revoking all of a user’s tokens, issuing different tokens with different rights, and so on, as our applications' requirements change. If we want, we can issue different tokens with different expiry times - say, a 120 day token for a user who runs our app on a kiosk, and a 15 minute token when the user logs in from a new IP. Finally, we don’t have to do any password hashing or user lookup by email or username when we receive a token, which can increase speeds.

First, a basic user model - I’ll not mention how the user is persisted, but assume that it is. To secure the password, we can use the bcrypt-ruby gem.

1
2
3
4
5
6
7
8
9
10
11
12
class User
  include BCrypt
  attr_accessor :email, :password_hash, :token

  def password
    @password ||= Password.new(password_hash)
  end

  def password=(password)
    self.password_hash = BCrypt::Password.create(password)
  end
end

The ruby BCrypt gem is great, because it lets us do password comparison between regular strings and the encoded hash automagically. So, our login check can be as simple as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class App < Sinatra::Base

  before do
    begin
      if request.body.read(1)
        request.body.rewind
        @request_payload = JSON.parse request.body.read, { symbolize_names: true }
      end
    rescue JSON::ParserError => e
      request.body.rewind
      puts "The body #{request.body.read} was not JSON"
    end
  end

  post '/login' do
    params = @request_payload[:user]

    user = User.find(email: params[:email])
    if user.password == params[:password] #compare the hash to the string; magic
      #log the user in
    else
      #tell the user they aren't logged in
    end
  end
end

The before do block is just some magic to parse a JSON request body - you can replace it as you desire if you’d rather accept non-JSON requests, auth details in headers, or something else. If the user is logged in, we want to generate a token, so let’s do that:

1
2
3
4
5
6
7
8
9
class User
  #...

  def generate_token!
    self.token = SecureRandom.urlsafe_base64(64)
    self.save! #persist
  end

end

The SecureRandom.urlsafe_base64 method just generates a nice, long, random, url-safe string. There is a similar base64 method that doesn’t guarantee url-safety; for web applications, there is no real disadvantage to making it url-safe. Whether or not you make a generate_token! method that does the saving itself or defer persistence to the controller is up to you.

Now, we can finish out our controller method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class App < Sinatra::Base

  post '/login' do
    params = @request_payload[:user]

    user = User.find(email: params[:email])
    if user.password == params[:password] #compare the hash to the string; magic
      user.generate_token!
      {token: user.token}.to_json # make sure you give hte user the token
    else
      #tell the user they aren't logged in
    end
  end

end

Now we need to be able to protect API routes and restrict them to logged in users. All we need to do is check the token that the user sends us to see if it is valid. Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
class App < Sinatra::Base

  def authenticate!
    @user = User.find(token: @request_payload[:token])
    halt 403 unless @user
  end

  get "/protected" do
    authenticate!
    do_something_special_with_user(@user)
  end

end

If the user has a valid token, we’ll be able to pull up their record, set the user record as appropriate, and do something. If not, halt and send them a 403 Unauthorized response - by calling halt, Sinatra will make sure nothing more gets executed from that route handler.

From here, you can add features as desired - accept the token in a HTTP header instead of in the request body (good for when clients might do pre-flight HEAD requests), expire the tokens automatically based on their age, create a delete action that removes tokens, allow users to have multiple tokens, associate certain access rights with a token, and so on, all within the same basic framework. Just make sure you put your web service behind TLS!

Happy coding!


I'm looking for better ways to build software businesses. Find out if I find something.