Skip to content

Commit

Permalink
Merge pull request #19 from ResultsMayVary/add_counter_based_auth
Browse files Browse the repository at this point in the history
Added Counter based OTP (HOTP)
  • Loading branch information
robertomiranda committed Feb 26, 2015
2 parents 0805410 + 5585d4c commit 0fa31d0
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 19 deletions.
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ class User < ActiveRecord::Base
end
```


## Usage

The has_one_time_password statement provides to the model some useful methods in order to implement our TFA system. AMo:Otp generates one time passwords according to [RFC 4226](http://tools.ietf.org/html/rfc4226) and the [HOTP RFC](http://tools.ietf.org/html/draft-mraihi-totp-timebased-00). This is compatible with Google Authenticator apps available for Android and iPhone, and now in use on GMail.
Expand Down Expand Up @@ -98,6 +97,53 @@ sleep 30 # lets wait again
user.authenticate_otp('186522', drift: 60) # => true
```

## Counter based OTP

An additonal counter field is required in our ``User`` Model

```ruby
rails g migration AddCounterForOtpToUsers otp_counter:integer
=>
invoke active_record
create db/migrate/20130707010931_add_counter_for_otp_to_users.rb
```

In addition set the counter flag option to true

```ruby
class User < ActiveRecord::Base
has_one_time_password counter_based: true
end
```

And for a custom counter column

```ruby
class User < ActiveRecord::Base
has_one_time_password counter_based: true, counter_column_name: :my_otp_secret_counter_column
end
```

Authentication is done the same. You can manually adjust the counter for your usage or set auto_increment on success to true.

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

When retrieving an ```otp_code``` you can also pass the ```auto_increment``` option.

```ruby
user.otp_code # => '186522'
user.otp_code # => '186522'
user.otp_code(auto_increment: true) # => '768273'
user.otp_code(auto_increment: true) # => '002811'
user.otp_code # => '002811'
```

## Google Authenticator Compatible

The library works with the Google Authenticator iPhone and Android app, and also includes the ability to generate provisioning URI's to use with the QR Code scanner built into the app.
Expand Down
76 changes: 59 additions & 17 deletions lib/active_model/one_time_password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,27 @@ module OneTimePassword
module ClassMethods

def has_one_time_password(options = {})

cattr_accessor :otp_column_name
class_attribute :otp_digits
cattr_accessor :otp_column_name, :otp_counter_column_name
class_attribute :otp_digits, :otp_counter_based

self.otp_column_name = (options[:column_name] || "otp_secret_key").to_s
self.otp_digits = options[:length] || 6

self.otp_counter_based = (options[:counter_based] || false)
self.otp_counter_column_name = (
options[:counter_column_name] || "otp_counter"
).to_s

include InstanceMethodsOnActivation

before_create { self.otp_regenerate_secret if !self.otp_column}
before_create do
self.otp_regenerate_secret if !otp_column
self.otp_regenerate_counter if otp_counter_based && !otp_counter
end

if respond_to?(:attributes_protected_by_default)
def self.attributes_protected_by_default #:nodoc:
super + [self.otp_column_name]
super + [otp_column_name, otp_counter_column_name]
end
end
end
Expand All @@ -29,29 +36,56 @@ def otp_regenerate_secret
self.otp_column = ROTP::Base32.random_base32
end

def otp_regenerate_counter
self.otp_counter = 1
end

def authenticate_otp(code, options = {})
totp = ROTP::TOTP.new(self.otp_column, {digits: self.otp_digits})
if drift = options[:drift]
totp.verify_with_drift(code, drift)
if otp_counter_based
hotp = ROTP::HOTP.new(otp_column, digits: otp_digits)
result = hotp.verify(code, otp_counter)
if result && options[:auto_increment]
self.otp_counter += 1
save if !new_record?
end
result
else
totp.verify(code)
totp = ROTP::TOTP.new(otp_column, digits: otp_digits)
if drift = options[:drift]
totp.verify_with_drift(code, drift)
else
totp.verify(code)
end
end
end

def otp_code(options = {})
if options.is_a? Hash
time = options.fetch(:time, Time.now)
padding = options.fetch(:padding, true)
if otp_counter_based
if options[:auto_increment]
self.otp_counter += 1
save if !new_record?
end
ROTP::HOTP.new(otp_column, digits: otp_digits).at(self.otp_counter)
else
time = options
padding = true
if options.is_a? Hash
time = options.fetch(:time, Time.now)
padding = options.fetch(:padding, true)
else
time = options
padding = true
end
ROTP::TOTP.new(otp_column, digits: otp_digits).at(time, padding)
end
ROTP::TOTP.new(self.otp_column, {digits: self.otp_digits}).at(time, padding)
end

def provisioning_uri(account = nil,options={})
def provisioning_uri(account = nil, options = {})
account ||= self.email if self.respond_to?(:email)
ROTP::TOTP.new(self.otp_column,options).provisioning_uri(account)

if otp_counter_based
ROTP::HOTP.new(otp_column, options).provisioning_uri(account)
else
ROTP::TOTP.new(otp_column, options).provisioning_uri(account)
end
end

def otp_column
Expand All @@ -61,6 +95,14 @@ def otp_column
def otp_column=(attr)
self.send("#{self.class.otp_column_name}=", attr)
end

def otp_counter
self.send(self.class.otp_counter_column_name)
end

def otp_counter=(attr)
self.send("#{self.class.otp_counter_column_name}=", attr)
end
end
end
end
2 changes: 1 addition & 1 deletion lib/active_model/otp/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module ActiveModel
module Otp
VERSION = "1.1.0"
VERSION = "1.2.0"
end
end

0 comments on commit 0fa31d0

Please sign in to comment.