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

35
spec/support/authenticated_model_helper.rb

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