Rails Token Authentication Without Devise

A popular solution for token-based authentication in Rails has been retired, and the most common replacements leave some security issues unaddressed. Here’s a solution with a clean and maintainable design.

Anyone working on a modern Rails application is probably familiar with Devise. It’s by far the most popular drop-in solution for handling authentication of the username and password variety. Anyone who also needs to offer token-based authentication (maybe in order to offer a REST API), might be accustomed to using Devise for this too, through its handy token_authenticatable feature.

Those developers might be dismayed, as I was, to find that token_authenticatable has been removed from recent versions of Devise. They might be further dismayed to find that the only solution that has any sort of consensus as a replacement seems dubious for a couple of different reasons.

The Problem and Solutions So Far

Why was token_authenticatable removed? According to the official blog post, the concern was raised that authentication tokens (along with other non-password secrets like password reset tokens) have traditionally been stored in plain text by Devise, and as such are vulnerable to timing attacks. This is true, and they are also vulnerable to brute-force cracking attempts, as well as being completely exposed in the event of unauthorized read access to the database.

Of course, all these issues have been long known as they pertain to the traditional user passwords that are at the core of Devise’s functionality. That’s why Devise has always run these passwords through a password hash function before storing them in the database. A good password hash function, such as the widely-trusted bcrypt that Devise uses by default, solves all these problems at once, and there’s very little reason not to use one when handling any data that is used as a password.

As of version 3.1, Devise began protecting most of its non-password secrets by hashing them with bcrypt as well. But hashing an authentication token that’s intended for repeated use presents a couple of extra challenges. For one, if bcrypt authentication needs to be performed on every request to an API, the extra performance penalty could result in a significant slowdown. Also, the user experience around API-enabled websites is often designed so that a user can retreive their API token if they’ve forgotten it, which is impossible if the token is only stored as a hash.

The Devise team decided not to impose a single solution to these problems on everyone who uses Devise for token authentication. Instead, they removed the feature and linked to a gist that presents two different code samples as starting points for a custom solution to the problem. One of these is equivalent to Devise’s old behavior, and the other one adds some protection against timing attacks at the expense of changing the application API and requiring additional info from the users who are authenticating via token.

On the plus side, this approach encourages users to think through the solution as it applies to their individual applications, rather than blindly copying code. On the other hand, it still amounts to the strongly discouraged practice of rolling one’s own security code. Furthermore, the provided “secure” option only addresses the timing attack problem, while continuing to store authentication secrets in plain text and thus remaining exposed to the other vulnerabilities.

An Alternate Solution

When facing these issues recently on a new API-focused application, I found some opportunities to improve on the solution suggested by the Devise team. Most importantly, I thought it was worth finding a way around the performance and token-recoverability issues mentioned above, to gain the benefit of using bcrypt to hash authentication tokens. In addition to protecting against more vulnerabilities than just timing attacks, this also means that while the overall security architecture is hand-rolled, the most critical hashing and comparison code is handed off to a trusted library.

In addition, I ended up with a solution that has a more modular and maintainable design for the data model, and that’s easily adaptable to applications not using Devise. Let’s walk through the solution below; feel free to make use of it and make suggestions for further improvement.

Storing the Secret

It’s common practice to add additional authentication-related fields to the application’s User model, but let’s defy that trend and create a separate AuthenticationToken model. This will avoid conflicting with any fields already on User, but more importantly, it decouples the User model from the token authentication logic. This is very good for maintainability, because changes to the authentication scheme won’t require changes to the User model (or models, as the case may be).

It’s also good because it’s flexible enough to allow multiple AuthenticationTokens for each User. Later we’ll discuss one reason why we might want that. But for now, it means that we can’t rely on a User-specific piece of data (such as email address, as in the Devise example) to uniquely identify the token value (which we’ll call the secret) that we hashed with bcrypt. So we’ll need some kind of searchable unique token identifier stored in plain text, and we’ll call that the secret_id. AWS users are used to keeping track of both a “key id” and a “secret key” for exactly this reason.

As long as we’re aiming for a modular design, we should also make the user-to-token relationship polymorphic, so we’ll only need one token model in applications that have multiple user models (to which we’ll give the general label authenticatable). With all that in mind, here’s the migration for the token model:

class CreateAuthenticationTokens < ActiveRecord::Migration
  def change
    create_table :authentication_tokens do |t|
      t.references :authenticatable, polymorphic: true
      t.string :secret_id
      t.string :hashed_secret
      t.timestamps
    end
    add_index :authentication_tokens, :secret_id, unique: true
  end
end

The Model

The secret_id and hashed_secret go in the database, while the plaintext secret is only stored in memory. We need code in our AuthenticationToken model to generate all three of these values, and also code to perform the actual authentication by finding a token that matches the secret and secret_id from a user request.

We’ll make the AuthenticationToken generate all three of these values before validation any time they don’t exist, which ensures that they’ll exist on newly-created tokens after saving. Here’s the code for our AuthenticationToken model:

require "securerandom"
require "bcrypt"

class AuthenticationToken < ActiveRecord::Base
  belongs_to :authenticatable, polymorphic: true
  validates :secret_id, presence: true, uniqueness: true
  validates :hashed_secret, presence: true
  before_validation :generate_secret_id, unless: :secret_id
  before_validation :generate_secret, unless: :secret
  attr_accessor :secret

  def self.find_authenticated credentials
    token = where(secret_id: credentials[:secret_id]).first
    token if token && token.has_secret?(credentials[:secret])
  end

  def has_secret? secret
    BCrypt::Password.new(hashed_secret) == secret
  end

  private

  def generate_secret_id
    begin
      self.secret_id = SecureRandom.hex 8
    end while self.class.exists?(secret_id: self.secret_id)
  end

  def generate_secret
    self.secret = SecureRandom.urlsafe_base64 32
    self.hashed_secret = BCrypt::Password.create secret, cost: cost
  end

  def cost
    Rails.env.test? ? 1 : 10
  end
end

To authenticate, we’ll just search for a token matching the given secret_id, and then ask bcrypt to compare its hashed_secret against the secret provided by the user. The result will be the matching AuthenticationToken object, or nil if there’s no match. Next, we just need a couple of private methods to generate the secret and secret_id.

We also have code to generate the all the necessary secret-related values at validation time. This ensures that when we save a newly-created AuthenticationToken, it will have all the necessary values (the plaintext secret will only be in memory, while the secret_id and hashed_secret will get saved to the database). We can use Ruby’s handy SecureRandom to generate both the secret and the secret_id, and we’ll give the secret considerably more entropy to make sure it’s secure. We also insert a little extra-paranoid protection when generating the secret_id, to make sure it’s unique.

Why does the secret_id need any entropy at all? In fact, why not just use AuthenticationToken’s existing id field? Because although it suits our purposes by being unique, it also gives away information about the chronological sequence of token creation and the total number of tokens in our database. This information may or may not be useful to potential attackers, but in general it’s not the business of anyone who hasn’t authenticated.

Finally, we confront the API performance issue by carefully choosing the cost value to pass in to bcrypt. In test mode, we want to use the smallest value possible so our automated tests can generate and authenticate tokens quickly. In production we’re starting with the same default of 10 that Devise uses, but we’re free to turn it up for extra security, or down for faster performance. If we turn it down all the way to 1 for a negligible performance overhead in production, we’ll still have hashed secrets that are much more secure than plain text.

The Controller

It’s common with or without Devise to have a current_user method in the controller to represent the logged-in user. We’ll follow that convention and add a current_token method that holds an AuthenticationToken, if any is currently authenticated. Then the controller can check for a current_token on each request, and automatically log in any associated user. Here’s the code to be added to ApplicationController:

class ApplicationController < ActionController::Base
  before_filter :authenticate_from_token

  protected

  def authenticate_from_token
    if current_token.try :authenticatable
      sign_in token.authenticatable, store: false
    end
  end

  def current_token
    AuthenticationToken.find_authenticated({
      secret: (params[:secret] || request.headers[:secret]),
      secret_id: (params[:secret_id] || request.headers[:secret_id]),
    })
  end
end

The current_token method includes the logic to find the credentials being submitted by the user. Here, it’s set up to recognize credentials in either the params or the request headers, although that policy can easily be customized.

The authenticate_from_token method might also need to be modified for different applications. The example above assumes Devise is present, so it uses Devise’s sign_in method to sign in the user and set current_user. The store: false option prevents the user’s identity from being saved in the session, so subsequent API requests will still require the secret and secret_id. This code could be easily modified to directly set current_user, or whatever is most appropriate for an application not using Devise.

Communicating Secrets to the User

Each user has to know his/her secret_id and secret in order to log in. How to best communicate these credentials to users depends on the needs of your particular application. Some applications might allow users to view their API credentials from a web page. Others might only send the credentials via email, or in response to other API calls (such as those that create new user accounts).

Note, however, that since we’re hashing our secrets, the only time we can show the user a secret is at the time it is first created. There’s no way for a user to come back later and ask for a secret that’s been forgotten. This is better for security, but it needs to be anticipated by the UI design of our application. Some services handle this by requiring the API token to be replaced (and any existing token invalidated) any time the user needs to retrieve it, but our decoupled design allows you to create multiple valid tokens for each user if you want. Whatever solution you plan to implement, you can use the console to verify that it’s easy to retrieve the credentials of a newly-created token:

$ rake db:migrate && rails console
> token = AuthenticationToken.create; [token.secret_id, token.secret]
 ...
 => ["42aa20ee181a2201", "hWIW41mF1wvvN_3TC5ObaFXBrdWPdEJBWjnGduuGwmA"]

But when the same token is freshly loaded from the database, its secret is unknown.

> token = AuthenticationToken.find(token.id); [token.secret_id, token.secret]
 ...
 => ["42aa20ee181a2201", nil]

Conclusion

The above code comes with no guarantee of security, and you should use it (along with any modifications you make to suit your own application) with caution, especially because it hasn’t been vetted by real-world use. But since it’s built around a core of bcrypt, and it aims for a decoupled, maintainable object-oriented design, it should be a good starting point for a post-Devise solution for token authentication. Try it on for size, and let me know what you think.

comments powered by Disqus