Rails + Grape + API Key Authentication

When adding a Grape API to an existing Rails application you have a few options for user authentication. Probably the simplest and most basic way to authenticate an API is by issuing API keys to your users.

How it works…

  1. User submits their Username/Password to the API.
  2. API authenticates the user and returns an API Key.
  3. For all subsequent API requests, the user submits the key with their request. This key authenticates them.

This is great because it protects the users credentials by taking them out of every API call. The API Key can also expire. So if it is compromised, access will only be available for as long as the key is active.

How to do it…

To start: I’m going to assume that you have an existing Rails application with a Grape API and user authentication.

1. Create an API Key model

Create a new model with the following attributes:

  • access_token
  • expires_at
  • user_id
  • active
rails g model api_key access_token:string expires_at:datetime user_id:integer active:boolean

2. Add indexes

Now take a look at the migration that rails just created for you. Add indexes on api_key and user_id since those will be queried often. It should look like this:

class CreateApiKeys < ActiveRecord::Migration
  def change
    create_table :api_keys do |t|
      t.string :access_token,      null: false
      t.integer :user_id,          null: false
      t.boolean :active,           null: false, default: true
      t.datetime :expires_at

      t.timestamps
    end

    add_index :api_keys, ["user_id"], name: "index_api_keys_on_user_id", unique: false
    add_index :api_keys, ["access_token"], name: "index_api_keys_on_access_token", unique: true
  end
end

Make sure you run the migration

rake db:migrate

3. Generate token

Go to your api key model (api_key.rb). Replace what you have there with the following.

class ApiKey < ActiveRecord::Base
  attr_accessible :access_token, :expires_at, :user_id, :active, :application
  before_create :generate_access_token
  before_create :set_expiration
  belongs_to :user

  def expired?
    DateTime.now >= self.expires_at
  end

  private
  def generate_access_token
    begin
      self.access_token = SecureRandom.hex
    end while self.class.exists?(access_token: access_token)
  end

  def set_expiration
    self.expires_at = DateTime.now+30
  end
end

This takes care of the creation of the access_token and setting the expiration date.

Please note that I have this setup to belong to User. You might need to change that based on how your existing authentication is setup.

4. Add Authentication helpers to Grape

Now, you need to setup a way for the user to create a new API key and then use it for authentication for other API calls.

Add the following helper methods to your Grape API file.

helpers do
    def authenticate!
      error!('Unauthorized. Invalid or expired token.', 401) unless current_user
    end

    def current_user
      # find token. Check if valid.
      token = ApiKey.where(access_token: params[:token]).first
      if token && !token.expired?
        @current_user = User.find(token.user_id)
      else
        false
      end
    end
end

5. Issue and test API Keys

Finally, you need a method to distribute and test api keys.

Here, I’ve implemented POST /api/auth. It takes in the users credentials and if correct returns a new key.

I’ve also implemented GET /api/ping. Which tests if the key is correct and returns “pong.”

# /api/auth
resource :auth do

  desc "Creates and returns access_token if valid login"
  params do
    requires :login, type: String, desc: "Username or email address"
    requires :password, type: String, desc: "Password"
  end
  post :login do

    if params[:login].include?("@")
      user = User.find_by_email(params[:login].downcase)
    else
      user = User.find_by_login(params[:login].downcase)
    end

    if user && user.authenticate(params[:password])
      key = ApiKey.create(user_id: user.id)
      {token: key.access_token}
    else
      error!('Unauthorized.', 401)
    end
  end

  desc "Returns pong if logged in correctly"
  params do
    requires :token, type: String, desc: "Access token."
  end
  get :ping do
    authenticate!
    { message: "pong" }
  end
end