Open-source project
heapsource/active_model_otp avatar
heapsource/active_model_otp

active_model_otp: two factor authentication as a model concern

Adds methods to set and authenticate against one time passwords (Two-Factor Authentication). Inspired in AM::SecurePassword

821 stars84 forksRubyMIT

At a glance

What is it?
active_model_otp is a MIT licensed Ruby gem that adds one time password two factor authentication to an ActiveModel, in the same spirit as the has_secure_password macro, with TOTP and HOTP generation, drift handling, replay protection and backup codes. The judgement is that the gem does one job with a small surface, and that the two design decisions you have to make yourself are the storage of the shared secret and the window you accept on expired codes.
Who is it for?
active_model_otp fits a Rails application that already leans on ActiveModel and wants two factor authentication as a macro on the model rather than as service code, especially one that also needs SMS delivery, since the drift option and the counter based mode are the parts other implementations tend to leave out.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 176 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A macro instead of a service object

ActiveModel::Otp is built to make adding Two Factor Authentication to a model simple, and the README is explicit that it is inspired by AM::SecurePassword, the Rails macro that has handled password hashing for years. The framing matters because it tells you the shape of the API. Rather than a service object that wraps your user class, you add a single macro, has_one_time_password, to the model, and the methods appear on the model itself. The walkthrough in the README is written for Rails 5.0, and it notes that the gem is also compatible with Rails 4.x versions, so it is not tied to the newest ActiveModel. The dependency list is short, with ROTP at 6.2.0 or higher doing the actual cryptography and Ruby 2.3 as the floor. The library is Ruby, and the package is published as a gem with a version badge on the README.

The secret column and the macro that uses it

Setting up a model takes three steps. First the column, which is generated with the Rails migration generator:

ruby
rails g migration AddOtpSecretKeyToUsers otp_secret_key:string

The migration is then applied with rake db:migrate, and the model gains the macro:

ruby
class User < ApplicationRecord
  has_one_time_password
end

For an existing table, the README gives the backfill, since every user needs a secret before two factor can be switched on:

ruby
User.find_each { |user| user.update_attribute(:otp_secret_key, User.otp_random_secret) }

Two options let you fit it to an existing schema, with column_name pointing the gem at a differently named field and length setting the number of digits in the generated codes, so a model with its own secret column and four digit codes is a one line change:

ruby
class User < ApplicationRecord
  has_one_time_password column_name: :my_otp_secret_column, length: 4
end

Codes, drift and what Google Authenticator expects

The generated codes follow TOTP as defined in RFC 6238 and HOTP as defined in RFC 4226, which is what makes the output compatible with the Google Authenticator apps for Android and iPhone. The secret is generated and saved automatically when a record is created, so a new user has a usable secret without an extra step. Getting the current code is a method call, and it can be computed for a different moment, which is what you need if your server clock and the phone clock disagree:

ruby
user.otp_code # => '186522'
sleep 30
user.otp_code # => '850738'

# Override current time
user.otp_code(time: Time.now + 3600) # => '317438'

Authentication is the other half, and it is where the drift option earns its place. A plain check succeeds once and then fails after the code has aged out, while a check with a drift of 60 seconds accepts a code that is slightly old, which the README links to SMS delivery where the delay between sending and typing is real:

ruby
user.authenticate_otp('186522', drift: 60) # => true

Stopping a code from being replayed

A time based code stays valid for a window, and by default nothing stops someone from submitting the same code twice inside that window. The fix the README describes is to keep track of the last time a user's code was verified, which requires one more integer column:

ruby
rails g migration AddLastOtpAtToUsers last_otp_at:integer

and the model has to point at it, with the after_column_name option:

ruby
class User < ApplicationRecord
  has_one_time_password after_column_name: :last_otp_at
end

With that in place, the same code cannot be used a second time, since the first authentication records the time and the second attempt is rejected. The default interval window is 30 seconds, and the README notes that this is the setting that matters when codes are delivered by SMS, which is commonly combined with the drift option to extend the life of the code. Two migrations and one option is a modest price for closing a replay hole, and it is the piece most hand rolled implementations of one time passwords leave out.

Counter based codes for a different threat model

The gem also supports a counter based mode, which is the HOTP half of the protocol, and it needs one more field:

ruby
rails g migration AddCounterForOtpToUsers otp_counter:integer

The column defaults to zero, which the README sets with change_column on the users table, and the model switches the mode on with counter_based: true, with counter_column_name available if the field is called something else. Authentication is unchanged, and you can adjust the counter by hand or let the library move it forward for you with auto_increment: true on success, which is the behaviour that makes counter based codes work as a one time sequence rather than a repeating code:

ruby
user.authenticate_otp('186522', auto_increment: true) # => true
user.authenticate_otp('186522') # => false

The same auto_increment option works when reading a code, so the counter advances as you generate, and you can decrement it yourself if a delivery fails.

Backup codes for the phone that is not there

The third mode is a set of backup codes stored on the model, which is the usual answer to a user who loses the device that holds the authenticator. It needs a text column:

ruby
rails g migration AddOtpBackupCodesToUsers otp_backup_codes:text

The array is stored using an array type in the schema or a serialized attribute in the model, and the README suggests looking at a library such as lockbox if you want the field encrypted. The column name is configurable with backup_codes_column_name. By default the library generates 12 backup codes, which backup_codes_count can change, and each code can be reused an unlimited number of times unless you pass one_time_backup_codes: true, in which case a backup code is consumed like a one time password. Either way the codes are checked with the same authenticate_otp method, which keeps the calling code in your controllers simple.

The gem itself, and how it has moved

The repository is a conventional gem layout, with lib for the code, test for the suite, gemfiles for appraisal matrices, a Rakefile, a Gemfile, the gemspec named active_model_otp.gemspec, a CHANGELOG.md, a LICENSE.txt and the GitHub configuration. The README badges show a continuous integration workflow, a gem version badge and a review badge from the Hound service, which dates the tooling. The licence is MIT, which is the right choice for a library you drop into a Rails application, and On releases, the tags are v2.3.1 from October 2021, v2.3.2 from April 2023 and v2.3.4 from April 2024, so the line has been stable for a while. The last push was on 2026-05-02 and the repository is not archived, so the work continues on the main branch even though the released versions have not moved recently, and the CHANGELOG.md is the place to check what changed before you bump the version in your Gemfile.

Editorial conclusion

active_model_otp fits a Rails application that already leans on ActiveModel and wants two factor authentication as a macro on the model rather than as service code, especially one that also needs SMS delivery, since the drift option and the counter based mode are the parts other implementations tend to leave out. It is the wrong choice if you need hardware token support or push based approval, because the README describes codes and authenticator apps only, and it is a poor fit if you cannot store a shared secret per user, since that column is the foundation of the whole scheme. Before you adopt it, generate the secret column with the migration generator, backfill existing accounts with User.otp_random_secret, and decide whether a code may be replayed inside the 30 second window, because by default the gem does not stop that unless you track the last verification time.

Frequently asked questions

How do I add two factor authentication to a Rails model with active_model_otp?

Add a string column for the secret with the migration generator, run rake db:migrate, and add has_one_time_password to the model. The secret is generated and saved automatically when a record is created, and existing accounts are backfilled with User.otp_random_secret.

How do I accept a slightly old one time password?

Pass the drift option to authenticate_otp, so user.authenticate_otp('186522', drift: 60) still succeeds after the code has aged out. The README ties this to SMS delivery, where the delay between sending and typing makes the default window too tight.

How do I stop a one time password from being reused?

Add an integer last_otp_at column and point the macro at it with after_column_name: :last_otp_at. The gem then records the time of the last successful verification and rejects a second use of the same code inside the default 30 second window, and backup codes can be made single use with one_time_backup_codes.

Official sources

  1. heapsource/active_model_otp on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/heapsource-active-model-otp.svg)](https://hysenlabs.com/projects/heapsource-active-model-otp)

Was this useful?