RESTful Authentication

I work on a Rails application with a standard HTML interface and a REST interface, both of which are served by the same RESTful controllers. Most of the actions used by these interfaces require some form of authentication and authorization.

Like most web applications, this one takes a traditional session-based authentication approach for the HTML interface: The user is directed to a login form if the session doesn’t indicate that he or she has successfully gone through the authentication procedure, and that login form then allows the user to enter a user name and password to identify him or her self to the system. This approach doesn’t work in the REST case — there simply isn’t any session (or cookies, for that matter) available to hold information between calls. Instead, some form of credentials must be sent along with each call made by the REST clients. There are many ways to accomplish this, including making some form of API key or ticket a mandatory part of all requests. On the server side, these tickets are associated with particular identities and authorizations, allowing the controllers to determine if the call should be allowed to go through or not.

The HTTP protocol’s built-in Basic Authentication scheme fits the ticket requirement perfectly. Under this scheme, the client is expected to send a set of credentials encoded in the HTTP AUTHORIZATION header along with each request. The format is

username:password

as a Base 64 encoded string (which also implies that the credentials are essentially sent as clear text — you probably need to protect them from prying eyes through the use of SSL (see ssl_required)). 

In our application, REST calls are made on behalf of a specific user so rather than equipping ticket entities with their own set of authorizations etc., we simply associate them with the user acting as their patron. When a REST call comes in, the application looks up the ticket in the database, finds the associated user and uses his or her authorizations to determine wether or not the call should be allowed to continue. 

When a user needs a ticket he or she creates one on a special support page in the application. The new ticket is associated with the user and will inherit his or her authorizations. The ticket has a unique name (supplied by the user) and a large random number (generated by the application) as token. These two values are in effect the HTTP Basic Authentication user name and password. However, to avoid storing the actual passwords in the database, we instead store a hashed form of the token along with some salt (to make it harder to reverse engineer the hash from the values in the database.)

So to summarize: the REST client sends the ticket name and token in the AUTHORIZATION header. When the call arrives on the server, the ticket is retrieved from the database. If found, and if hashing the token with the salt stored in the ticket in the database yields a hashed version of the token identical to the one stored in the database, then the ticket is authenticated and auhtorization can begin.

Details, details, details

Tickets are represented by the RestTicket model shown below. Every ticket has a unique name, a hashed token, a patron name and a nip of salt. In our application we also store the IP address from which the ticket was last used, and the time at which this happened. These extra data help tracking down problems reported by users and assists administrators in finding stale tickets that haven’t been used in a long time.

require 'rails_generator/secret_key_generator'
 
class RestTicket < ActiveRecord::Base
  validates_presence_of :name
  validates_uniqueness_of :name
  validates_presence_of :hashed_token 
  validates_uniqueness_of :hashed_token
  validates_presence_of :patron_name
  validates_presence_of :salt
 
  def set_token_and_salt(cleartext)
    self.salt = object_id.to_s + rand.to_s
    self.hashed_token = RestTicket.encrypt_token(cleartext, salt)
  end
 
  def has_token(cleartext)
    hashed_token == RestTicket.encrypt_token(cleartext, salt)
  end
 
  def patron
    User.find_by_name(patron_name)
  end
 
  private 
 
  def self.encrypt_token(token, salt) 
    salted = token + "&+Va10=/(H_*" + salt 
    Digest::SHA1.hexdigest(salted) 
  end 
end

One central goal for our implementation was to keep authentication code out of the controllers. A good way to achieve this is through filters added by the ApplicationController:

class ApplicationController < ActionController::Base
  ...
  before_filter :enforce_authentication
  ...
 
  # Enforces authentication for both ordinary requests and REST requests. For ordinary
  # requests a cookie+session based approach is used. For REST requests a HTTP Basic 
  # Authentication + RestTicket based approach is used.
 
  def enforce_authentication
    if is_rest_call?
      enforce_rest_authentication
    else
      enforce_non_rest_authentication
    end
  end
 
  protected
 
  def enforce_rest_authentication
    authenticate_or_request_with_http_basic('MyApplication') do |ticket_name, ticket_token|
      ticket = RestTicket.find_by_name(ticket_name)
      if ticket
        ticket.last_used_at = Time.now
        ticket.last_used_from_ip = request.remote_ip
        ticket.save
      end
 
      if ticket && ticket.has_token(ticket_token)
        @the_rest_ticket = ticket
        true
      else
        @the_rest_ticket = nil
        false
      end
    end
  end
 
  def enforce_non_rest_authentication
    unless session[:user]
      respond_to do |format|
        format.html do
          session[:original_uri] = request.request_uri
          flash[:error] = "Please log in"
          redirect_to :controller => :front_page
        end
 
        format.xml do
          # Getting here is a serious error as enforce_authentication should
          # have sent us to enforce_rest_authentication. Freak out and run:
          # render_rest_error is an application specific helper which assist
          # in rendering nice REST error responses with the right HTTP status
          # status code and a resonable xml response.
 
          render_rest_error "Authentication failed", :status => :error
        end
      end
    end
 
    true
  end
 
  private
 
  # Determines if the incoming request is a REST call (defined as a request that would
  # target respond_to :xml blocks.
 
  def is_rest_call?
    mime_type_priority = Array(Mime::Type.lookup_by_extension(request.parameters[:format]) || request.accepts)
    mime_type_priority[0].to_sym == :xml
  end
end

Notice how easy Basic Authentication is activated with the built-in authenticate_or_request_with_http_basic method.

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Slashdot
This entry was posted in Rails and tagged , , . Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.

2 Comments

  1. benvds
    Posted January 11, 2011 at 21:08 | Permalink

    Great! I was looking for some simple token rest authentication.

  2. Posted August 13, 2011 at 12:24 | Permalink

    Impressive. It helps a lot. Thanks.

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*