Browse Source

Seperate totp secret generation from confirmation

For most use cases the totp secret needs to be transmitted
to the end user so it its helpful to be able to generate
it, before confirming it.
master
Sam Clegg 9 years ago
parent
commit
06c67df575
  1. 23
      lib/two_factor_authentication/models/two_factor_authenticatable.rb
  2. 42
      spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb

23
lib/two_factor_authentication/models/two_factor_authenticatable.rb

@ -33,19 +33,20 @@ module Devise
end end
def authenticate_totp(code, options = {}) def authenticate_totp(code, options = {})
raise "authenticate_totp called with no otp_secret_key set" if otp_secret_key.nil? totp_secret = options[:otp_secret_key] || otp_secret_key
totp = ROTP::TOTP.new( digits = options[:otp_length] || self.class.otp_length
otp_secret_key, digits: options[:otp_length] || self.class.otp_length
)
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?
totp = ROTP::TOTP.new(totp_secret, digits: digits)
totp.verify_with_drift(code, drift) totp.verify_with_drift(code, drift)
end end
def provisioning_uri(account = nil, options = {}) def provisioning_uri(account = nil, options = {})
raise "provisioning_uri called with no otp_secret_key set" if otp_secret_key.nil? totp_secret = options[:otp_secret_key] || otp_secret_key
options[:digits] ||= options[:otp_length] || self.class.otp_length
raise "provisioning_uri called with no otp_secret_key set" if totp_secret.nil?
account ||= email if respond_to?(:email) account ||= email if respond_to?(:email)
ROTP::TOTP.new(otp_secret_key, options).provisioning_uri(account) ROTP::TOTP.new(totp_secret, options).provisioning_uri(account)
end end
def need_two_factor_authentication?(request) def need_two_factor_authentication?(request)
@ -73,8 +74,14 @@ module Devise
respond_to?(:otp_secret_key) && !otp_secret_key.nil? respond_to?(:otp_secret_key) && !otp_secret_key.nil?
end end
def confirm_totp_secret(secret, code, options = {})
return false unless authenticate_totp(code, {otp_secret_key: secret})
self.otp_secret_key = secret
true
end
def generate_totp_secret def generate_totp_secret
self.otp_secret_key = ROTP::Base32.random_base32 ROTP::Base32.random_base32
end end
def create_direct_otp(options = {}) def create_direct_otp(options = {})

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

@ -122,7 +122,7 @@ describe Devise::Models::TwoFactorAuthenticatable do
describe 'with secret set' do describe 'with secret set' do
before do before do
instance.email = 'houdini@example.com' instance.email = 'houdini@example.com'
instance.generate_totp_secret instance.otp_secret_key = instance.generate_totp_secret
end end
it "returns uri with user's email" do it "returns uri with user's email" do
@ -157,13 +157,10 @@ describe Devise::Models::TwoFactorAuthenticatable do
shared_examples 'generate_totp_secret' do |klass| shared_examples 'generate_totp_secret' do |klass|
let(:instance) { klass.new } let(:instance) { klass.new }
it 'populates otp_secret_key column' do it 'returns a 16 character string' do
original_key = instance.otp_secret_key secret = instance.generate_totp_secret
instance.generate_totp_secret expect(secret).to match(/\w{16}/)
expect(instance.otp_secret_key).to match(/\w{16}/)
expect(instance.otp_secret_key).to_not eq(original_key)
end end
end end
@ -171,6 +168,37 @@ describe Devise::Models::TwoFactorAuthenticatable do
it_behaves_like 'generate_totp_secret', EncryptedUser it_behaves_like 'generate_totp_secret', EncryptedUser
end end
describe '#confirm_totp_secret' do
shared_examples 'confirm_totp_secret' do |klass|
let(:instance) { klass.new }
let(:secret) { instance.generate_totp_secret }
let(:totp_helper) { TotpHelper.new(secret, instance.class.otp_length) }
it 'populates otp_secret_key column when given correct code' do
instance.confirm_totp_secret(secret, totp_helper.totp_code)
expect(instance.otp_secret_key).to match(secret)
end
it 'does not populate otp_secret_key when when given incorrect code' do
instance.confirm_totp_secret(secret, '123')
expect(instance.otp_secret_key).to be_nil
end
it 'returns true when given correct code' do
expect(instance.confirm_totp_secret(secret, totp_helper.totp_code)).to be true
end
it 'returns false when given incorrect code' do
expect(instance.confirm_totp_secret(secret, '123')).to be false
end
end
it_behaves_like 'confirm_totp_secret', GuestUser
it_behaves_like 'confirm_totp_secret', EncryptedUser
end
describe '#max_login_attempts' do describe '#max_login_attempts' do
let(:instance) { build_guest_user } let(:instance) { build_guest_user }

Loading…
Cancel
Save