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.