Token Authentication with Rails

Nick Charlton

Sometimes you don’t need a complex authentication approach for a project. Let’s say that you’re working on the first iteration of a mobile app with a Rails-based API. You need to authenticate a user, but you don’t want to implement something like OAuth 2.

Rails has this built-in, but it doesn’t seem to be that well known. We’ve never written about it, and I’ve found myself finding it very useful again recently.

It’s called authenticate_or_request_with_http_token.

Implementation

module Api
  class BaseController < ApplicationController
    before_action :authenticate

    private

    def authenticate
      authenticate_or_request_with_http_token do |token, _options|
        User.find_by(token: token)
      end
    end

    def current_user
      @current_user ||= authenticate
    end
  end
end

On every request, we take out the token then try and find a user associated with it.

Unfortunately, this approach exposes you to a timing attack. There’s not a good, general solution to this problem so you should think about it before implementing something similar.

In fitting with the typical pattern of implementing current_user, we do the same here too.

As an example, we might talk to an API implemented this way with curl like this:

curl -H "Authorization: Token abc123" http://localhost:3000/api/test

Testing

We might go about testing this like so:

require "rails_helper"

RSpec.describe "Authenticating with the API" do
  before do
    Rails.application.routes.draw do
      get "/api/test" => "test#index"
    end
  end

  after do
    Rails.application.reload_routes!
  end

  context "when the user provides a valid api token" do
    it "allows the user to pass" do
      create(:user, token: "sekkrit")
      credentials = authenticate_with_token("sekkrit")

      get "/api/test", headers: { "Authorization" => credentials }

      expect(response).to be_successful
      expect(response.body).to eq({ "message" => "Hello world!" }.to_json)
    end
  end

  context "when the user provides an invalid api token" do
    it "does not allow to user to pass" do
      create(:user, token: "sekkrit")
      credentials = authenticate_with_token("less-sekkrit")

      get "/api/test", headers: { "Authorization" => credentials }

      expect(response).to be_unauthorized
    end
  end

  private

  TestController = Class.new(Api::BaseController) do
    def index
      render json: { message: "Hello world!" }
    end
  end

  def authenticate_with_token(token)
    ActionController::HttpAuthentication::Token.encode_credentials(token)
  end
end

To isolate our test here from the rest of our application, we’re drawing a route just for this example. Then we can test both successful and unsuccessful responses as a request spec.

There’s also an example Rails project on GitHub which shows this action.