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…
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
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
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.
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.
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
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.
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!
Writer. Musician. Adventurer. Nerd.
Purveyor of GIFs and dad jokes.