Mimimal API Authentication on Rails

We’ve been building a lot of Ember.js authentication mechanisms for several projects, and we’re starting to think that we’ve got it down to a fine art. We’ve been treating my front-end apps as simply an API consumer, which means that they need a similar method of authenticating their API access as that of a tradition (service-based) API client.

A lot of our current thinking on how to put this together can be found in Blogomat, an example application We’re using as a test-bed for ideas of how to build apps with Ember and Rails.

Let’s have a look at our requirements for how we’re going to authorise API client accesses:

  • Some sort of short-lived session token which can be acquired by the API client to use when accessing API resources to authorise the client.
  • A way of authenticating a user with their username and password to attach it to our session token.
  • A way of authenticating a service with a secret that is shared by the server and the client, without that secret being transmitted over the wire.

Some additional design goals include:

  • Write the minimum amount needed to implement the required features without adding any overly complicated dependencies to our application (ie Devise).
  • Authentication methanisms should be easily understood so that those writing API clients don’t have to struggle to understand what’s going on.
  • Try and stay as close to RESTful ideals, and compatible with JSON API as possible.

So, in Blogomat, we have a simple (non-versioned) API namespace in my routes file:

1
2
3
4
5
Blogomat::Application.routes.draw do
  namespace :api, defaults: {format: :json} do
    resource :sessions, only: [:create, :show, :destroy]
  end
end

Seems straight forward enough, let’s create our Api::SessionsController. The first thing we do is create a abstract ApiController class, which (much like the conventional ApplicationController can encapsulate shared behaviour across all API endpoints).

1
2
3
4
5
6
class Api::SessionsController < ApiController

  def create
  end

end

API Session Tokens

We know at this point that we’re going to need some sort of api token object for the Api::SessionsController to create and return, let’s think about this object a little. What do we know about it from the requirements?

  • We know that it needs some sort of unique identifier.
  • We know that it is short-lived, so needs to have a TTL.
  • We know that it needs to be able to refer to a user object.

One thing we don’t know is that it needs to be stored in the database, and in fact, since we know that we will be checking this token every time we receive an API request, it’s probably overkill.

We also think that it would be nice if we could store tokens in some sort of fast, shared storage so that if we need to scale our app horizontally we have that capability without massively changing the underlying authorisation scheme. In this vein, we have chosen to store the session tokens in Redis, however you could choose to use Memcache or even just in a Hash, should you want (and in fact, in my tests we have it using a faked-out Redis client backed by a Hash).

The main reason we chose Redis is that it’s reasonably ubiquitous, and has the handy ability to expire keys, so that we don’t have to worry garbage collecting our ApiSessionTokens. This example simply uses a hash to cut it down to a reasonable size to demo.

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class ApiSessionToken
  TTL = 20.minutes

  def self.store
    @store ||= Hash.new
  end

  def initialize(existing_token=nil)
    @token = existing_token
    self.last_seen = Time.now unless expired?
  end

  def token
    @token ||= MicroToken.generate 128
  end

  def ttl
    return TTL unless last_seen
    elapsed   = Time.now - last_seen
    remaining = (TTL - elapsed).floor
    remaining > 0 ? remaining : 0
  end

  def last_seen
    store[:last_seen_at]
  end

  def last_seen=(as_at)
    store[:last_seen_at] = as_at
  end

  def user
    return if expired?
    store[:user]
  endo

  def user=(user)
    store[:user] = user
  end

  def expired?
    ttl < 1
  end

  def valid?
    !expired?
  end

  private

  def store
    self.class.store[token] ||= {}
  end
end

Now that we have an object we can use, let’s alter our sessions controller to return it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Api::SessionsController < ApiController

  def create
    if params[:username]
      @user = User.find_by_username(params[:username])
      token.user = @user if _provided_valid_password? || _provided_valid_api_key?
    end

    respond_with token
  end

  private

  def _provided_valid_password?
    params[:password] == 'foo password'
  end

  def _provided_valid_api_key?
    params[:api_key] == 'foo key'
  end

end

So this is pretty cool. Now if we have receive a POST it’ll return a valid API session token, which will last for 20 minutes and then drop off the map after that.

If they post again with a username and password, or username and api key then we’ll let then sign in as that user for the duration of their session, provided they have provided valid credentials.

Now we can go back and write some helpers in ApiController to make working with these sessions easier, and add a before filter to protect API actions from access without a session token.

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
26
27
28
29
30
31
32
33
34
35
36
class ApiController < ApplicationController

  before_filter :api_session_token_authenticate!

  private

  def signed_in?
    !!current_api_session_token.user
  end

  def current_user
    current_api_session_token.user
  end

  def api_session_token_authenticate!
    return _not_authorized unless _authorization_header && current_api_session_token.valid?
  end

  def current_api_session_token
    @current_api_session_token ||= ApiSessionToken.new(_authorization_header)
  end

  def _authorization_header
    request.headers['HTTP_AUTHORIZATION']
  end

  def _not_authorized message = "Not Authorized"
    render json: {error: message}, status: 401
  end

end

class Api::SessionsController < ApiController
  skip_before_filter :api_session_token_authenticate!, only: [:create]

end

Password Authentication

The next task is to get password authentication working. We’ve decided to run with the herd and use bcrypt, which saves us having to write our own hashing algorythm. Phew!

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

  validates_presence_of   :username,      on: :create
  validates_uniqueness_of :username,      on: :create
  validates_presence_of   :email_address, on: :create
  validates_uniqueness_of :email_address, on: :create
  validates_presence_of   :password,      on: :create

  def password=(secret)
    write_attribute(:password, BCrypt::Password.create(secret))
  end

end

Now we can create a UserAuthenticationService to encapsulate our authentication logic. Basically we just delegate to bcrypt-ruby.

1
2
3
4
5
6
7
module UserAuthenticationService
  module_function

  def authenticate_with_password(user, attempt)
    user && BCrypt::Password.new(user.password) == attempt
  end
end

And wire it up to our sessions controller.

1
2
3
4
5
6
7
class Api::SessionController < ApiController

  def _provided_valid_password?
    params[:password] && UserAuthenticationService.authenticate_with_password!(@user, params[:password])
  end

end

Great! Now our user an send an API request to authenticate with their password in order to be able to access the API.

API Key Authentication

The next step is to provide a way of authenticating with a shared-secret, which isn’t transmitted between the client and the API during normal API usage. This sort of authentication is usually used by services trying to consume your API, rather than users themselves.

So, our design goals are as follows:

  • Based on a secret shared between the client and server.
  • Must be easily computed by both the server and client.
  • Must be unique enough that it’s not succeptable to a MITM or Replay attack.

Of course, your API is running over SSL, right?!

With our requirements mapped out, we can implmenent our API key authentication and wire it up to our controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module UserAuthenticationService
  module_function

  def authenticate_with_api_key(user, key, current_token)
    user && key && current_token && OpenSSL::Digest::SHA256.new("#{user.username}:#{user.api_secret}:#{current_token}") == key
  end
end

class Api::SessionsController < ApiController

  def _provided_valid_api_key?
    params[:api_key] && UserAuthenticationService.authenticate_with_api_key!(@user, params[:api_key], current_api_session_token.token)
  end

end

Summary

This has been a much longer blog post than we initially sat down to write, so we’ll stop here. We plan to follow up soon with an entry about implementing the client-side API session token logic in Ember.js.

Thanks for taking the time to read all this!

Comments