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. 35
      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|

35
spec/support/authenticated_model_helper.rb

@ -32,23 +32,24 @@ module AuthenticatedModelHelper
silence_stream(STDOUT) do
ActiveRecord::Schema.define(version: 1) do
create_table 'users', force: :cascade do |t|
t.string 'email', default: '', null: false
t.string 'encrypted_password', default: '', null: false
t.string 'reset_password_token'
t.datetime 'reset_password_sent_at'
t.datetime 'remember_created_at'
t.integer 'sign_in_count', default: 0, null: false
t.datetime 'current_sign_in_at'
t.datetime 'last_sign_in_at'
t.string 'current_sign_in_ip'
t.string 'last_sign_in_ip'
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.integer 'second_factor_attempts_count', default: 0
t.string 'nickname', limit: 64
t.string 'otp_secret_key'
t.string 'direct_otp'
t.datetime 'direct_otp_sent_at'
t.string 'email', default: '', null: false
t.string 'encrypted_password', default: '', null: false
t.string 'reset_password_token'
t.datetime 'reset_password_sent_at'
t.datetime 'remember_created_at'
t.integer 'sign_in_count', default: 0, null: false
t.datetime 'current_sign_in_at'
t.datetime 'last_sign_in_at'
t.string 'current_sign_in_ip'
t.string 'last_sign_in_ip'
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.integer 'second_factor_attempts_count', default: 0
t.string 'nickname', limit: 64
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