Token-Based API Authentication

A friend recently came to me wondering how he could add token-based authentication to his API.

I used Devise for my app, but it looks like they removed token auth. I’ve found a few gems, but they all look to do more than I need.

I’ve been critical of Devise for a long time. I used it exclusively on my earliest Rails sites because of how popular it was and how powerful it was out of the box. But trying to customize Devise is its biggest downside. In this instance, Devise is percevied as your authentication gatekeeper – and homage must be paid.

The good news is: feel free to keep Devise around, but you don’t need it for your API. We can use some relatively unknown methods built right into Ruby and Rails. Here’s how…

Migrations

We’re going to assign a token to each user. Once the client signs in, they’ll receive a token that can then be used for all authenticated API calls. Let’s get that onto the users table.

$ rails g migration add_auth_token_to_users auth_token

This creates the following migration for us:

class AddAuthTokenToUsers < ActiveRecord::Migration
  def change
    add_column :users, :auth_token, :string
    add_index :users, :auth_token
  end
end

We’re going to be looking up users based on that auth_token, so that add_index method is very important here.

Run the migration:

$ rake db:migrate

The Base Controller

I’ve built plenty of Rails-based API’s in my time. The first step for me is usually to make sure I have a good “base” controller for all API controllers. This allows me to set things up apart from ApplicationController, which typically is heavily bloated with methods that I don’t need in my API. In this case, it will allow me to set custom authentication methods outside of Devise.

class Api::ApiController < ActionController::Base
end

There’s a lot to digest here, so let’s break it down:

First, we need a way to protect our controllers:

  def require_login!
    return true if authenticate_token
    render json: { errors: [ { detail: "Access denied" } ] }, status: 401
  end

We’re stubbing out a authenticate_token method here for now – we want to encapsulate that logic in a separte method. But if it returns a user, we’re all set. Otherwise, we return an error message and a 401 status.

We know that the vast majority of our controllers are not accessible without authentication, so we can run the require_login! method as a before_action in this controller. If we need to skip it for certain endpoints (like sign-in), we can use a skip_before_action there.

So now we can move on to the authenticate_token method:

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

Rails has the authenticate_with_http_token method built-in. It’ll handle all the details for us here – we just need to know how to lookup the user with the token that’s passed in as a header.

Next, we’ll finish this up with some helper methods like current_user and user_signed_in?.

The finished controller:

# app/controllers/api/base_controller.rb

class Api::BaseController < ActionController::Base
  before_action :require_login!
  helper_method :person_signed_in?, :current_user

  def user_signed_in?
    current_person.present?
  end

  def require_login!
    return true if authenticate_token
    render json: { errors: [ { detail: "Access denied" } ] }, status: 401
  end

  def current_user
    @_current_user ||= authenticate_token
  end

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

end

Signing In

So how do we get the token for the user? Just like a normal sessions controller – but we’ll return the token instead of handling setting session info and performing redirects.

First, we’ll add some routes:

# config/routes.rb

  namespace :api, :defaults => {:format => :json} do
    as :user do
      post   "/sign-in"       => "sessions#create"
      delete "/sign-out"      => "sessions#destroy"
    end
  end

And the controller:

class Api::SessionsController < Api::BaseController
  skip_before_action :require_login!, only: [:create]

  def create
    resource = User.find_for_database_authentication(:email => params[:user_login][:email])
    resource ||= User.new

    if resource.valid_password?(params[:user_login][:password])
      auth_token = resource.generate_auth_token
      render json: { auth_token: auth_token }
    else
      invalid_login_attempt
    end

  end

  def destroy
    resource = current_person
    resource.invalidate_auth_token
    head :ok
  end

  private
    def invalid_login_attempt
      render json: { errors: [ { detail:"Error with your login or password" }]}, status: 401
    end
  end

We’re using two Devise methods in the create method here: find_for_database_authentication and valid_password?. If you’re not using Devise, you can easily replace these with your own authentication system. The key here is to load and verify the user. Once this is done, we’ll ask the User model to give us a token. Note that we’re delegating this responsibilty to the User model – this is not the job of the controller!

Likewise for destroying this token, we provide a sign out method. We’ve stubbed out invalidate_auth_token on the User which we can build next.

Please also note that we don’t immediately error if the user is not found – we’ll load User.new and still check the password even if it’s blank. This prevents attackers from timing our response times to determine if an email is valid or no.

User Model

In the sessions controller, we stubbed out two methods on the User model – one to generate a token and one to invalidate this token. Let’s fill those in:

# app/models/user.rb

  def generate_auth_token
    token = SecureRandom.hex
    self.update_columns(auth_token: token)
    token
  end

  def invalidate_auth_token
    self.update_columns(auth_token: nil)
  end

We’re using SecureRandom to generate a random hexadecimal string. This will generate a 32 character string. For example:

irb(main):001:0> 10.times { p SecureRandom.hex }
"c46890f205b82c1b74c750c3dce43223"
"0cda5c4fd3b4e9536b823696fbd8874b"
"8d8d96a8dc575b7d3659acf8e37aee64"
"60d5f9a3586da5290bdbfab88ec6e47a"
"25a6c19009805f89c71b4a9079a3585f"
"336b75f35e60f987b477b852c29989f3"
"849dcecdf685712517d9f5d0a7793138"
"554992f0b9cabb68b9aa8a90070da259"
"e5159948971bf835deb59df0f5ac3efe"
"5dd10aca5fea289027228f422898f48f"

generate_auth_token will generate a new token, update the database, and return the token to be used by our SessionsController. invalidate_auth_token will simply set this value to nil and disallow the User to be authenticated with this token in the future.

API Usage

Now that we’ve got all the moving parts, let’s test things out with curl.

# Initial Authorization
$ curl -X POST --data "user_login%5Bemail%5D=jon%40mccartie.com&user_login%5Bpassword%5D=please1234" https://localhost:5000/api/sign-in.json
{"auth_token":"a88601e054ba3df53bf9c9ff2d0d24f9"}

# Protected Calls
$ curl -H "Authorization: Token token=a88601e054ba3df53bf9c9ff2d0d24f9" https://localhost:5000/api/people.json
[{"id":3,"name":"Lonzo McDermott","profile":...

# Example Failed Authentication
$ curl -H "Authorization: Token token=abc" https://localhost:5000/api/people.json
{"errors":[{"detail":"Access denied"}]}

# Sign out (returns blank 200 response)
$ curl -X DELETE -H "Authorization: Token token=a88601e054ba3df53bf9c9ff2d0d24f9" https://localhost:5000/api/sign-out.json

Expiration

Maybe you don’t like the idea of these auth tokens living on indefinitely. We can easily add some logic to expire these tokens.

First, we need to know when the token was generated at. So we add a token_created_at field to User:

$ rails g migration add_token_created_at_to_users token_created_at:datetime

Note: now we’ll be looking up users now not just by auth_token, but by auth_token and token_created_at. Let’s make sure to add a compound index:

class AddTokenCreatedAtToUsers < ActiveRecord::Migration
  def change
    add_column :users, :token_created_at, :datetime
    remove_index :users, :auth_token
    add_index :users, [:auth_token, :token_created_at]
  end
end

Next, let’s make sure we touch this attribute when we create and destroy tokens:

# app/models/user.rb

  def generate_auth_token
    token = SecureRandom.hex
    self.update_columns(auth_token: token, token_created_at: Time.zone.now)
    token
  end

  def invalidate_auth_token
    self.update_columns(auth_token: nil, token_created_at: nil)
  end

Now in our base controller, we can check to make sure the token is still “fresh”. Should we validate the user, then check the token_created_at value? Gadzooks, no! Let’s make our database do that.

# app/controllers/api/base_controller

def authenticate_token
  authenticate_with_http_token do |token, options|
    User.where(auth_token: token).where("token_created_at >= ?", 1.month.ago).first
  end
end

The API client will get the unauthorized response and can attempt to sign in again to fetch a fresh token.

Conclusion

As Ruby developers, we have a littany of incredible Gems available to us. And when faced with the question “Should I build this myself? Or use a gem?”, there’s usually incredible value to not re-inventing the wheel. But make sure your reliance on other libraries never blinds you to simpler solutions that can be fully customized to your needs. Sometimes “rolling your own” can actually save you time.

Happy coding!

Hi there, I'm Jon.

Writer. Musician. Adventurer. Nerd.

Purveyor of GIFs and dad jokes.