Easy Rails API Authentication Using restful-authentication

You’ve got a Rails application and you’d like to add an API. This tutorial assumes you are using restful-authentication.

Creating the API key

You’ll need to add a new column, api_key, to your users table. This field will store the unique API key used for authentication.

./script/generate migration AddApiKeyToUsers
class AddApiKeyToUsers < ActiveRecord::Migration
  def self.up
    add_column :users, :api_key, :string, :limit => 40, :default => ""
  end
 
  def self.down
    remove_column :users, :api_key
  end
end

The API key is a simple SHA hash based on the current time and a random number — we need this to be unique. We’re not going to enable the API for a user by default, though you could easily do this using before_create. Instead, we’ll let the user enable or disable the API using the API Keys Controller, which we’ll create next.

Make the following changes to your app/models/user.rb file:

class User < ActiveRecord::Base
 
  def enable_api!
    self.generate_api_key!
  end
 
  def disable_api!
    self.update_attribute(:api_key, "")
  end
 
  def api_is_enabled?
    !self.api_key.empty?
  end
 
  protected
 
    def secure_digest(*args)
      Digest::SHA1.hexdigest(args.flatten.join('--'))
    end
 
    def generate_api_key!
      self.update_attribute(:api_key, secure_digest(Time.now, (1..10).map{ rand.to_s }))
    end
 
end

Now, let’s create the API Keys Controller, which we’ll use to allow the user to create, re-generate, and delete their API Key. This effectively allows the user to enable and disable API access to their account.

./script/generate controller APIKeys
class APIKeysController < ApplicationController
  before_filter :login_from_cookie
  before_filter :login_required
 
  # Create or re-generate the API key
  def create
    current_user.enable_api!
 
    respond_to do |format|
      format.html { redirect_to edit_user_path(current_user) }
    end
  end
 
  # Delete the API key
  def destroy
    current_user.disable_api!
 
    respond_to do |format|
      format.html { redirect_to edit_user_path(current_user) }
    end
  end
 
end

Add the resource to config/routes.rb

map.resource :api_key

You’ll need to allow the user to enable and disable the API, as well as re-generate their API key if they feel it’s been compromised. Add something like this to app/views/users/edit.html.erb:

<% if @user.api_is_enabled? %>
  <p>
    Your API Key:
    (<%= link_to "re-generate", api_key_path, :method => :post %> | <%= link_to "disable", api_key_path, :method => :delete %>)
  </p>
  <p>
    <strong><%= @user.api_key %></strong>
  </p>
<% else %>
  <p>
    You'll need a unique key to make calls to the API.  Remember to keep this key a secret as it can be used to access your account.
  </p>
  <p>
    <%= link_to("Get a key", api_key_path, :method => :post) %>
  </p>
<% end %>

Authenticating the API Key

Add the following to lib/authenticated_system.rb:

def login_from_api_key
  self.current_user = User.find_by_api_key(params[:api_key]) unless params[:api_key].empty?
end

Modify lib/authenticated_system.rb to add login_from_api_key as follows:

def current_user
  @current_user ||= (login_from_session || login_from_api_key || login_from_basic_auth || login_from_cookie) unless @current_user == false
end

Optionally, if you’d also like to support access to the API via HTTP Basic Authentication using the API key in addition to a user’s login and password, you can make the following change to app/models/user.rb:

def self.authenticate(login, password)
  return nil if login.blank? || password.blank?
  if password.downcase == "x"
    # This is an API request
    u = find_by_api_key(login)
  else
    u = find_by_login(login.downcase)
    u && u.authenticated?(password) ? u : nil
  end
end

When prompted to log in using HTTP Basic Authentication, use the API Key as the login and ‘X’ as the password.

Testing it Out

Assuming you have a RESTful resource called Items, you should be able to use curl as so:

curl http://www.yoursite.com/items/1.xml?api_key=356a192b
Saturday, May 16th, 2009 Programming

7 Comments to Easy Rails API Authentication Using restful-authentication

  • Avishai says:

    Very cool. I’ve been working on a similar way to authenticate users to edit stuff they’ve posted to my app, without having to create an account. Turns out you can override the default REST routes to require some kind of token or key to authorize the action:

    map.resources :things do |thing|
    map.edit ‘/things/:id/edit/:verification_code’, :controller => “posts”, :action => “edit”
    map.confirm ‘/things/:id/confirm/:verification_code’, :controller => “things”, :action => “confirm”
    end

    Works fine in dev, but haven’t really tested it yet.

  • maheshbalaji says:

    use the above code ( API Authentication Using restful-authentication) but i am facing an 500 error and i have checked my development log it shows the errors like /simpletwitterapp/app/helpers/api_keys_helper.rb to define ApiKeysHelper is there any thing has to define in it

    • Alexandre Carvalho says:

      Using:

      ./script/generate controller APIKeys

      will give you some trouble because of the rails namings conventions

      Use:

      ./script/generate controller ApiKeys

      and you won’t have any problem like the one with the helper.

  • Awesome post. Very helpful. We’re rolling out some code at CrowdVine with this. Having it laid out meant it was easy to squeeze this feature in between other work. Thank you!

  • Thanx for sharing the code, it was pretty useful. But there is a security issue with the login_from_api_key method. The following finder finds a user, but it’s supposed to find none: User.find_by_api_key(”)

    So any user can authenticate itself by leaving the api_key parameter value blank. This is how i do it:

    def login_from_api_key
    self.current_user = User.valid.find_by_api_key(params[:api_key]) unless params[:api_key].blank?
    end

  • Alican says:

    Thanks for tutorial, I implemented correctly and working so far but
    I can access xmls without api key parameter passed.

    I.e
    /tasks.xml returning all tasks by XML format (need to asks for api key?)
    /tasks.xml?api_key=randomkeyhere returning tasks by XML format normally

    How can i restrict respond to xml blocks with only API key.
    Regards.

  • Leave a Reply

    Justin has been obsessing over writing simple Web software using Ruby on Rails since 2007. He's also an entrepreneur and Lean Startup expert. Learn more

    View Justin Britten's profile on LinkedIn

    Subscribe to Justin Britten's blog Follow Justin Britten on Twitter Network with Justin Britten on LinkedIn
     
    Prefinery: Simple, online beta management software'

    Launch a private beta for your Web application in minutes. Prefinery takes care of collecting e-mail addresses, generating invitation codes, and sending invitations for your private beta. Your customers never leave your site, and e-mail invitations are sent from your address.

    Justin is Founder and CEO of Prefinery.