caikesouza

Token Based Authentication in Rails

Token based authentication is when an API client uses a token identifier to make authenticated HTTP requests.

A lot of popular services offer token based authentication for connecting with their web API, like HipChat, Campfire, Backpack, Last.fm and many others. It’s not yet a standard, but there is an official draft that specifies the scheme.

image

Token based authentication offers many benefits over HTTP Basic and Digest Authentication:

  • More convenience, as we can easily expire or regenerate tokens without affecting the user’s account password.
  • Better security if compromised, since vulnerability is limited to API access and not the user’s master account.
  • The ability to have multiple tokens for each user, which they can use to grant access to different API clients.
  • Greater control for each token, so different access rules can be implemented.

Getting an API token usually means visiting a profile settings page on the service’s website and requesting an access key. Some might already have a key generated for us.

image

The Authorization header format for Token based authentication looks like so:

GET /episodes HTTP/1.1
Host: localhost:3000
Authorization: Token token=123123123

Rails Authentication

Rails offers the authenticate_or_request_with_http_token method, which automatically checks the Authorization request header for a token and passes it as an argument to the given block:

authenticate_or_request_with_http_token do |token, options|
  # authenticate user...
end

Inside that block is where we implement our authentication strategy. In the following example, we’ll authenticate our requests for the EpisodesController class.

class EpisodesController < ApplicationController
  before_action :authenticate

  def index
    episodes = Episode.all
    render json: episodes, status: 200
  end

  protected
    def authenticate
      authenticate_or_request_with_http_token do |token, options|
        User.find_by(auth_token: token)
      end
    end
end

Using a before_action, we call the authenticate_or_request_with_http_token method. We only care about the first argument, which is the token we’ll use to look up the user.

It is very important that the auth_token is unique across all users. In our User model, we use a before_create callback to generate the token.

class User < ActiveRecord::Base
  before_create :set_auth_token

  private
    def set_auth_token
      return if auth_token.present?

      begin
        self.auth_token = SecureRandom.hex
      end while self.class.exists?(auth_token: self.auth_token)
    end
end

The token generation code is placed inside a while loop. If SecureRandom returns a token that’s already being used, we’ll keep on looping until it generates one that’s unique.

If we wanted to take it one step further, we could also add a unique constraint on the auth_token column in the database.

Using curl, we can test our token based authentication by passing a valid token in the Authorization header:

$ curl -IH "Authorization: Token token=16d7d6089b8fe0c5e19bfe10bb156832" \
  http://localhost:3000/episodes
HTTP/1.1 200 OK 
Content-Type: application/json; charset=utf-8

Unauthorized

If the authentication fails and our block returns false, the request is halted and our application immediately responds with a 401 - Unauthorized status code.

$ curl -IH "Authorization: Token token=fake" http://localhost:3000/episodes.json
HTTP/1.1 401 Unauthorized 
Content-Type: text/html; charset=utf-8
WWW-Authenticate: Token realm="Application"

According to the HTTP spec, a 401 - Unauthorized response must include a WWW-Authenticate header with a challenge applicable to the requested resource. The authenticate_or_request_with_http_token automatically includes that header for us:

WWW-Authenticate: Token realm="Application"

The Token part means that the given resource uses token authentication. The resource under that URI is currently part of the “Application” realm. The realm value allows protected resources to be partitioned into different sets of protection spaces, each with its own access policies.

The default realm value used by Rails is “Application”. To change it to a more descriptive value, we can pass the new name as an argument to the authenticate_or_request_with_http_token method.

authenticate_or_request_with_http_token('Premium') do |token, options|
  User.find_by(auth_token: token)
end

Limitations

One limitation we might come across when using authenticate_or_request_with_http_token is the fact that this method doesn’t allow for much customization. For example, it always responds with the Content-Type set to HTML regardless of the mime type requested by the API client. There’s also no easy way to add a custom error message to the response body if we wanted to.

For more flexibility, we can use the authenticate_with_http_token method and manually build the response ourselves:

class EpisodesController < ApplicationController
  before_action :authenticate

  def index
    episodes = Episode.all
    render json: episodes, status: 200
  end

  protected
    def authenticate
      authenticate_token || render_unauthorized
    end

    def authenticate_token
      authenticate_with_http_token do |token, options|
        User.find_by(auth_token: token)
      end
    end

    def render_unauthorized
      self.headers['WWW-Authenticate'] = 'Token realm="Application"'
      render json: 'Bad credentials', status: 401
    end
end

And that gives us a proper JSON response:

$ curl -IH "Authorization: Token token=fake" http://localhost:3000/episodes/1.json
HTTP/1.1 401 Unauthorized 
Content-Type: application/json; charset=utf-8

For more information about token based authentication, the draft is available at http://tools.ietf.org/html/draft-hammer-http-token-auth-01. The code examples for this blog post are available on the BananaPodcast project on GitHub.

We will be covering this and other topics in our upcoming “Surviving APIs with Rails” course on Code School, which will be out in March.

Have you ever implemented a token based authentication on your Rails API ? We’d love to hear your opinion in our comments!

- Carlos Souza (@caike)

(photo source: http://www.flickr.com/photos/kolix/2539213620)

02.03.14 ← See All Posts
blog comments powered by Disqus