Browse Source

Prevent reuse of TOTP codes (#94)

This change updates the rotp version which now includes support
for preventing TOTP code reuse via tracking the timestamp
of the last used code.
master
Sam Clegg 9 years ago committed by Moncef Belyamani
parent
commit
2df6fa2481
  1. 8
      README.md
  2. 1
      lib/generators/active_record/templates/migration.rb
  3. 7
      lib/two_factor_authentication/models/two_factor_authenticatable.rb
  4. 4
      lib/two_factor_authentication/schema.rb
  5. 8
      spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb
  6. 3
      spec/rails_app/app/models/encrypted_user.rb
  7. 2
      spec/rails_app/app/models/guest_user.rb
  8. 1
      spec/support/authenticated_model_helper.rb
  9. 2
      two_factor_authentication.gemspec

8
README.md

@ -33,6 +33,7 @@ Note that Ruby 2.1 or greater is required.
### Installation
#### Automatic initial setup
To set up the model and database migration file automatically, run the
following command:
@ -48,8 +49,10 @@ migration in `db/migrate/`, which will add the following columns to your table:
- `:encrypted_otp_secret_key_salt`
- `:direct_otp`
- `:direct_otp_sent_at`
- `:totp_timestamp`
#### Manual initial setup
If you prefer to set up the model and migration manually, add the
`:two_factor_authentication` option to your existing devise options, such as:
@ -61,7 +64,7 @@ devise :database_authenticatable, :registerable, :recoverable, :rememberable,
Then create your migration file using the Rails generator, such as:
```
rails g migration AddTwoFactorFieldsToUsers second_factor_attempts_count:integer encrypted_otp_secret_key:string:index encrypted_otp_secret_key_iv:string encrypted_otp_secret_key_salt:string direct_otp:string direct_otp_sent_at:datetime
rails g migration AddTwoFactorFieldsToUsers second_factor_attempts_count:integer encrypted_otp_secret_key:string:index encrypted_otp_secret_key_iv:string encrypted_otp_secret_key_salt:string direct_otp:string direct_otp_sent_at:datetime totp_timestamp:timestamp
```
Open your migration file (it will be in the `db/migrate` directory and will be
@ -74,6 +77,7 @@ add_index :users, :encrypted_otp_secret_key, unique: true
Save the file.
#### Complete the setup
Run the migration with:
bundle exec rake db:migrate
@ -129,7 +133,7 @@ method on your model:
user.generate_totp_secret
```
This can then be shared via a provisioning uri:
This must then be shared via a provisioning uri:
```ruby
user.provisioning_uri # This assumes a user model with an email attribute

1
lib/generators/active_record/templates/migration.rb

@ -6,6 +6,7 @@ class TwoFactorAuthenticationAddTo<%= table_name.camelize %> < ActiveRecord::Mig
add_column :<%= table_name %>, :encrypted_otp_secret_key_salt, :string
add_column :<%= table_name %>, :direct_otp, :string
add_column :<%= table_name %>, :direct_otp_sent_at, :datetime
add_column :<%= table_name %>, :totp_timestamp, :timestamp
add_index :<%= table_name %>, :encrypted_otp_secret_key, unique: true
end

7
lib/two_factor_authentication/models/two_factor_authenticatable.rb

@ -16,7 +16,7 @@ module Devise
::Devise::Models.config(
self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length,
:remember_otp_session_for_seconds, :otp_secret_encryption_key,
:direct_otp_length, :direct_otp_valid_for)
:direct_otp_length, :direct_otp_valid_for, :totp_timestamp)
end
module InstanceMethodsOnActivation
@ -38,7 +38,10 @@ module Devise
drift = options[:drift] || self.class.allowed_otp_drift_seconds
raise "authenticate_totp called with no otp_secret_key set" if totp_secret.nil?
totp = ROTP::TOTP.new(totp_secret, digits: digits)
totp.verify_with_drift(code, drift)
new_timestamp = totp.verify_with_drift_and_prior(code, drift, totp_timestamp)
return false unless new_timestamp
self.totp_timestamp = new_timestamp
true
end
def provisioning_uri(account = nil, options = {})

4
lib/two_factor_authentication/schema.rb

@ -23,5 +23,9 @@ module TwoFactorAuthentication
def direct_otp_sent_at
apply_devise_schema :direct_otp_sent_at, DateTime
end
def totp_timestamp
apply_devise_schema :totp_timestamp, Timestamp
end
end
end

8
spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb

@ -70,10 +70,10 @@ describe Devise::Models::TwoFactorAuthenticatable do
end
describe '#authenticate_totp' do
shared_examples 'authenticate_totp' do |instance|
before :each do
instance.otp_secret_key = '2z6hxkdwi3uvrnpn'
instance.totp_timestamp = nil
@totp_helper = TotpHelper.new(instance.otp_secret_key, instance.class.otp_length)
end
@ -90,6 +90,12 @@ describe Devise::Models::TwoFactorAuthenticatable do
code = @totp_helper.totp_code(1.minutes.ago.to_i)
expect(do_invoke(code, instance)).to eq(false)
end
it 'prevents code reuse' do
code = @totp_helper.totp_code
expect(do_invoke(code, instance)).to eq(true)
expect(do_invoke(code, instance)).to eq(false)
end
end
it_behaves_like 'authenticate_totp', GuestUser.new

3
spec/rails_app/app/models/encrypted_user.rb

@ -8,7 +8,8 @@ class EncryptedUser
:encrypted_otp_secret_key_iv,
:encrypted_otp_secret_key_salt,
:email,
:second_factor_attempts_count
:second_factor_attempts_count,
:totp_timestamp
has_one_time_password(encrypted: true)
end

2
spec/rails_app/app/models/guest_user.rb

@ -5,7 +5,7 @@ class GuestUser
define_model_callbacks :create
attr_accessor :direct_otp, :direct_otp_sent_at, :otp_secret_key, :email,
:second_factor_attempts_count
:second_factor_attempts_count, :totp_timestamp
def update_attributes(attrs)
attrs.each do |key, value|

1
spec/support/authenticated_model_helper.rb

@ -49,6 +49,7 @@ module AuthenticatedModelHelper
t.string 'otp_secret_key'
t.string 'direct_otp'
t.datetime 'direct_otp_sent_at'
t.timestamp 'totp_timestamp'
end
end
end

2
two_factor_authentication.gemspec

@ -27,7 +27,7 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'rails', '>= 3.1.1'
s.add_runtime_dependency 'devise'
s.add_runtime_dependency 'randexp'
s.add_runtime_dependency 'rotp'
s.add_runtime_dependency 'rotp', '>= 3.2.0'
s.add_runtime_dependency 'encryptor'
s.add_development_dependency 'bundler'

Loading…
Cancel
Save