Token based authentication is when an API client uses a token identifier to make authenticated HTTP requests.
A lot of popular services offer token based authentication for connecting with their web API, like HipChat, Campfire, Backpack, Last.fm and many others. It’s not yet a standard, but there is an official draft that specifies the scheme.
Token based authentication offers many benefits over HTTP Basic and Digest Authentication:
- More convenience, as we can easily expire or regenerate tokens without affecting the user’s account password.
- Better security if compromised, since vulnerability is limited to API access and not the user’s master account.
- The ability to have multiple tokens for each user, which they can use to grant access to different API clients.
- Greater control for each token, so different access rules can be implemented.
Getting an API token usually means visiting a profile settings page on the service’s website and requesting an access key. Some might already have a key generated for us.
The Authorization header format for Token based authentication looks like so:
GET /episodes HTTP/1.1 Host: localhost:3000 Authorization: Token token=123123123
Rails offers the
authenticate_or_request_with_http_token method, which automatically checks the Authorization request header for a token and passes it as an argument to the given block:
authenticate_or_request_with_http_token do |token, options| # authenticate user... end
Inside that block is where we implement our authentication strategy. In the following example, we’ll authenticate our requests for the
class EpisodesController < ApplicationController before_action :authenticate def index episodes = Episode.all render json: episodes, status: 200 end protected def authenticate authenticate_or_request_with_http_token do |token, options| User.find_by(auth_token: token) end end end
before_action, we call the
authenticate_or_request_with_http_token method. We only care about the first argument, which is the token we’ll use to look up the user.
It is very important that the auth_token is unique across all users. In our User model, we use a before_create callback to generate the token.
require 'securerandom' class User < ActiveRecord::Base before_create :set_auth_token private def set_auth_token return if auth_token.present? self.auth_token = generate_auth_token end def generate_auth_token SecureRandom.uuid.gsub(/\-/,'') end end
The token generation process is delegated to
SecureRandom.uuid. This method is part of Ruby’s standard library, and returns a Universally Unique Identifier (RFC 4122) that’s guaranteed to be unique across a global namespace (the gsub call removes dashes so the token is URL friendly). And if we wanted to take it a step further, we could also add a database level constraint, but we’ll skip that for now.
Using curl, we can test our token based authentication by passing a valid token in the Authorization header:
$ curl -IH "Authorization: Token token=a47a8e54b11c4de5a4a351734c80a14a" \ http://localhost:3000/episodes HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8
If the authentication fails and our block returns false, the request is halted and our application immediately responds with a
401 - Unauthorized status code.
$ curl -IH "Authorization: Token token=fake" http://localhost:3000/episodes.json HTTP/1.1 401 Unauthorized Content-Type: text/html; charset=utf-8 WWW-Authenticate: Token realm="Application"
According to the HTTP spec, a
401 - Unauthorized response must include a WWW-Authenticate header with a challenge applicable to the requested resource. The
authenticate_or_request_with_http_token automatically includes that header for us:
WWW-Authenticate: Token realm="Application"
The Token part means that the given resource uses token authentication. The resource under that URI is currently part of the “Application” realm. The realm value allows protected resources to be partitioned into different sets of protection spaces, each with its own access policies.
The default realm value used by Rails is “Application”. To change it to a more descriptive value, we can pass the new name as an argument to the
authenticate_or_request_with_http_token('Premium') do |token, options| User.find_by(auth_token: token) end
One limitation we might come across when using
authenticate_or_request_with_http_token is the fact that this method doesn’t allow for much customization. For example, it always responds with the Content-Type set to HTML regardless of the mime type requested by the API client. There’s also no easy way to add a custom error message to the response body if we wanted to.
For more flexibility, we can use the
authenticate_with_http_token method and manually build the response ourselves:
class EpisodesController < ApplicationController before_action :authenticate def index episodes = Episode.all render json: episodes, status: 200 end protected def authenticate authenticate_token || render_unauthorized end def authenticate_token authenticate_with_http_token do |token, options| User.find_by(auth_token: token) end end def render_unauthorized self.headers['WWW-Authenticate'] = 'Token realm="Application"' render json: 'Bad credentials', status: 401 end end
And that gives us a proper JSON response:
$ curl -IH "Authorization: Token token=fake" http://localhost:3000/episodes/1.json HTTP/1.1 401 Unauthorized Content-Type: application/json; charset=utf-8
For more information about token based authentication, the draft is available at http://tools.ietf.org/html/draft-hammer-http-token-auth-01. The code examples for this blog post are available on the BananaPodcast project on GitHub.
Have you ever implemented a token based authentication on your Rails API ? We’d love to hear your opinion in our comments!
– Carlos Souza (@caike)
(photo source: http://www.flickr.com/photos/kolix/2539213620)
UPDATE – 2014-11-01: Updated the post to use SecureRandom.uuid