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 10 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 ### Installation
#### Automatic initial setup #### Automatic initial setup
To set up the model and database migration file automatically, run the To set up the model and database migration file automatically, run the
following command: following command:
@ -48,8 +49,10 @@ migration in `db/migrate/`, which will add the following columns to your table:
- `:encrypted_otp_secret_key_salt` - `:encrypted_otp_secret_key_salt`
- `:direct_otp` - `:direct_otp`
- `:direct_otp_sent_at` - `:direct_otp_sent_at`
- `:totp_timestamp`
#### Manual initial setup #### Manual initial setup
If you prefer to set up the model and migration manually, add the If you prefer to set up the model and migration manually, add the
`:two_factor_authentication` option to your existing devise options, such as: `: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: 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 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. Save the file.
#### Complete the setup #### Complete the setup
Run the migration with: Run the migration with:
bundle exec rake db:migrate bundle exec rake db:migrate
@ -129,7 +133,7 @@ method on your model:
user.generate_totp_secret user.generate_totp_secret
``` ```
This can then be shared via a provisioning uri: This must then be shared via a provisioning uri:
```ruby ```ruby
user.provisioning_uri # This assumes a user model with an email attribute 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 %>, :encrypted_otp_secret_key_salt, :string
add_column :<%= table_name %>, :direct_otp, :string add_column :<%= table_name %>, :direct_otp, :string
add_column :<%= table_name %>, :direct_otp_sent_at, :datetime 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 add_index :<%= table_name %>, :encrypted_otp_secret_key, unique: true
end end

7
lib/two_factor_authentication/models/two_factor_authenticatable.rb

@ -16,7 +16,7 @@ module Devise
::Devise::Models.config( ::Devise::Models.config(
self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length, self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length,
:remember_otp_session_for_seconds, :otp_secret_encryption_key, :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 end
module InstanceMethodsOnActivation module InstanceMethodsOnActivation
@ -38,7 +38,10 @@ module Devise
drift = options[:drift] || self.class.allowed_otp_drift_seconds drift = options[:drift] || self.class.allowed_otp_drift_seconds
raise "authenticate_totp called with no otp_secret_key set" if totp_secret.nil? raise "authenticate_totp called with no otp_secret_key set" if totp_secret.nil?
totp = ROTP::TOTP.new(totp_secret, digits: digits) 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 end
def provisioning_uri(account = nil, options = {}) def provisioning_uri(account = nil, options = {})

4
lib/two_factor_authentication/schema.rb

@ -23,5 +23,9 @@ module TwoFactorAuthentication
def direct_otp_sent_at def direct_otp_sent_at
apply_devise_schema :direct_otp_sent_at, DateTime apply_devise_schema :direct_otp_sent_at, DateTime
end end
def totp_timestamp
apply_devise_schema :totp_timestamp, Timestamp
end
end 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 end
describe '#authenticate_totp' do describe '#authenticate_totp' do
shared_examples 'authenticate_totp' do |instance| shared_examples 'authenticate_totp' do |instance|
before :each do before :each do
instance.otp_secret_key = '2z6hxkdwi3uvrnpn' instance.otp_secret_key = '2z6hxkdwi3uvrnpn'
instance.totp_timestamp = nil
@totp_helper = TotpHelper.new(instance.otp_secret_key, instance.class.otp_length) @totp_helper = TotpHelper.new(instance.otp_secret_key, instance.class.otp_length)
end end
@ -90,6 +90,12 @@ describe Devise::Models::TwoFactorAuthenticatable do
code = @totp_helper.totp_code(1.minutes.ago.to_i) code = @totp_helper.totp_code(1.minutes.ago.to_i)
expect(do_invoke(code, instance)).to eq(false) expect(do_invoke(code, instance)).to eq(false)
end 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 end
it_behaves_like 'authenticate_totp', GuestUser.new 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_iv,
:encrypted_otp_secret_key_salt, :encrypted_otp_secret_key_salt,
:email, :email,
:second_factor_attempts_count :second_factor_attempts_count,
:totp_timestamp
has_one_time_password(encrypted: true) has_one_time_password(encrypted: true)
end end

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

@ -5,7 +5,7 @@ class GuestUser
define_model_callbacks :create define_model_callbacks :create
attr_accessor :direct_otp, :direct_otp_sent_at, :otp_secret_key, :email, 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) def update_attributes(attrs)
attrs.each do |key, value| 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 'otp_secret_key'
t.string 'direct_otp' t.string 'direct_otp'
t.datetime 'direct_otp_sent_at' t.datetime 'direct_otp_sent_at'
t.timestamp 'totp_timestamp'
end end
end 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 'rails', '>= 3.1.1'
s.add_runtime_dependency 'devise' s.add_runtime_dependency 'devise'
s.add_runtime_dependency 'randexp' 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_runtime_dependency 'encryptor'
s.add_development_dependency 'bundler' s.add_development_dependency 'bundler'

Loading…
Cancel
Save