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.

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.