has_secure_token demo in Rails

Generating Secure Tokens on Your ActiveRecord Models

You must have used the `has_secure_password` macro in Rails. Did you know Rails also provides a `has_secure_token` macro to generate unique tokens on your models? In this article, we'll learn how it works and we'll also see how Rails implements it behind the scenes.

3 min read

Sometimes, you may want to use token-based authentication to grant your users access to some resource for some time, after which the token expires and needs to be regenerated.

You could manually create a token on your models as follows:

class User < ApplicationRecord
  before_create :generate_token
  
  private
  
  def generate_token
    self.token = SecureRandom.base58(24)
  end
end

However, a better solution is to use the has_secure_token macro provided by Rails. By default, it will use the name :token and length 24, but you can provide a different name and length for your token.

class User < ApplicationRecord
  has_secure_token :access_code, length: 30
end

Here's a passing test that shows how it works.

test 'user has a secure access code which can be regenerated' do
  user = create(:user)
  
  assert user.access_code  # rpWRC...
  
  assert_equal 30, user.access_code.length
  
  user.regenerate_access_code  # J6K9u...
end

Note: If you're using it for the first time, don't forget to create the backing column for your model.

class AddAccessCodeToUser < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :access_code, :string
  end
end

For the most part, you can be assured that the generated token will be unique. However, consider adding a unique index in the database just in case, to deal with this highly unlikely scenario.

Regenerating Tokens

Sometimes, for security reasons, you may want to expire the user's token or access code after some time. This is useful if they haven't been active on your application for a while and you want to make sure the old token isn't compromised.

You can regenerate the token using the regenerate_{token} method on your model.

def handle_inactive_member(user)
  user.regenerate_access_code
end

Behind the Scenes

If you open the secure_token.rb file in the Rails codebase, you'll notice that the ActiveRecord::SecureToken module is a Rails Concern.

Concerns in Rails: Everything You Need to Know
Concerns are an important concept in Rails that can be confusing to understand for those new to Rails as well as seasoned practitioners. This post explains what concerns are, how they work, and how & when you should use them to simplify your code, with practical, real-world examples.

Here's a simplified version of the source code.

# activerecord/lib/active_record/secure_token.rb

module ActiveRecord
  module SecureToken
    extend ActiveSupport::Concern

    module ClassMethods
      def has_secure_token(attribute = :token, length: MIN_LENGTH)
        require "active_support/core_ext/securerandom"
        
        define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token(length: length) }
        
        before_create { send("#{attribute}=", self.class.generate_unique_secure_token(length: length)) unless send("#{attribute}?") }
      end

      def generate_unique_secure_token(length: MIN_LENGTH)
        SecureRandom.base58(length)
      end
    end
  end
end

The very first thing to note is that Rails will only load the securerandom library when you actually use this macro. This library is an interface to secure random number generators.

When you call the has_secure_token macro, two things happen:

  1. It dynamically creates the regenerate_token method using the define_method in Ruby. All it does is update the existing token, setting a new value.
  2. Adds a before_create callback that fires before the model is saved. It calls the model.token= method, generating and setting a secure and unique token.

Finally, the generate_unique_secure_token method uses the SecureRandom class to generate a random base58 string with the given length.

That's it.


I hope you liked this article and you learned something new.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.