@ -2,121 +2,150 @@ require 'spec_helper'
include AuthenticatedModelHelper
include AuthenticatedModelHelper
describe Devise :: Models :: TwoFactorAuthenticatable do
describe Devise :: Models :: TwoFactorAuthenticatable do
describe '#otp_code' do
describe '#create_direct_otp' do
shared_examples 'otp_code' do | instance |
let ( :instance ) { build_guest_user }
subject { instance . otp_code ( time ) }
let ( :time ) { 1_392_852_456 }
it 'set direct_otp field' do
expect ( instance . direct_otp ) . to be_nil
instance . create_direct_otp
expect ( instance . direct_otp ) . not_to be_nil
end
it 'returns an error if no secret is set' do
it 'set direct_otp_send_at field to current time' do
expect { subject } . to raise_error Exception
Timecop . freeze ( ) do
instance . create_direct_otp
expect ( instance . direct_otp_sent_at ) . to eq ( Time . now )
end
end
end
context 'secret is set ' do
it 'honors .direct_otp_length ' do
before :each do
expect ( instance . class ) . to receive ( :direct_otp_length ) . and_return ( 10 )
instance . otp_secret_key = '2z6hxkdwi3uvrnpn'
instance . create_direct_otp
end
expect ( instance . direct_otp . length ) . to equal ( 10 )
it 'does not return an error' do
expect ( instance . class ) . to receive ( :direct_otp_length ) . and_return ( 6 )
subject
instance . create_direct_otp
end
expect ( instance . direct_otp . length ) . to equal ( 6 )
end
it 'matches Devise configured length' do
it " honors 'direct_otp_length' in options paramater " do
expect ( subject . length ) . to eq ( Devise . otp_length )
instance . create_direct_otp ( length : 8 )
end
expect ( instance . direct_otp . length ) . to equal ( 8 )
instance . create_direct_otp ( length : 10 )
expect ( instance . direct_otp . length ) . to equal ( 10 )
end
end
context 'with a known time' do
describe '#authenticate_direct_otp' do
let ( :time ) { 1_392_852_756 }
let ( :instance ) { build_guest_user }
it 'fails if no direct_otp has been set' do
expect ( instance . authenticate_direct_otp ( '12345' ) ) . to eq ( false )
end
it 'returns a known result' do
context 'after generating an OTP' do
expect ( subject ) .
before :each do
to eq ( '0000000524562202' . split ( / / ) . last ( Devise . otp_length ) . join )
instance . create_direct_otp
end
end
end
context 'with a known time yielding a result with less than 6 digits' do
it 'accepts correct OTP' do
let ( :time ) { 1_393_065_856 }
Timecop . freeze ( Time . now + instance . class . direct_otp_valid_for - 1 . second )
expect ( instance . authenticate_direct_otp ( instance . direct_otp ) ) . to eq ( true )
end
it 'returns a known result padded with zeroes' do
it 'rejects invalid OTP' do
expect ( subject ) .
Timecop . freeze ( Time . now + instance . class . direct_otp_valid_for - 1 . second )
to eq ( '0000001608007672' . split ( / / ) . last ( Devise . otp_length ) . join )
expect ( instance . authenticate_direct_otp ( '12340' ) ) . to eq ( false )
end
end
end
end
end
it_behaves_like 'otp_code' , GuestUser . new
it 'rejects expired OTP' do
it_behaves_like 'otp_code' , EncryptedUser . new
Timecop . freeze ( Time . now + instance . class . direct_otp_valid_for + 1 . second )
expect ( instance . authenticate_direct_otp ( instance . direct_otp ) ) . to eq ( false )
end
it 'prevents code re-use' do
expect ( instance . authenticate_direct_otp ( instance . direct_otp ) ) . to eq ( true )
expect ( instance . authenticate_direct_otp ( instance . direct_otp ) ) . to eq ( false )
end
end
end
end
describe '#authenticate_otp' do
describe '#authenticate_totp' do
shared_examples 'authenticate_otp' do | instance |
shared_examples 'authenticate_totp' do | instance |
before :each do
before :each do
instance . otp_secret_key = '2z6hxkdwi3uvrnpn'
instance . otp_secret_key = '2z6hxkdwi3uvrnpn'
@totp_helper = TotpHelper . new ( instance . otp_secret_key , instance . class . otp_length )
end
end
def do_invoke ( code , user )
def do_invoke ( code , user )
user . authenticate_otp ( code )
user . authenticate_t otp ( code )
end
end
it 'authenticates a recently created code' do
it 'authenticates a recently created code' do
code = instance . otp_code
code = @totp_helper . t otp_code
expect ( do_invoke ( code , instance ) ) . to eq ( true )
expect ( do_invoke ( code , instance ) ) . to eq ( true )
end
end
it 'does not authenticate an old code' do
it 'does not authenticate an old code' do
code = instance . otp_code ( 1 . minutes . ago . to_i )
code = @totp_helper . t otp_code( 1 . minutes . ago . to_i )
expect ( do_invoke ( code , instance ) ) . to eq ( false )
expect ( do_invoke ( code , instance ) ) . to eq ( false )
end
end
end
end
it_behaves_like 'authenticate_otp' , GuestUser . new
it_behaves_like 'authenticate_t otp' , GuestUser . new
it_behaves_like 'authenticate_otp' , EncryptedUser . new
it_behaves_like 'authenticate_t otp' , EncryptedUser . new
end
end
describe '#send_two_factor_authentication_code' do
describe '#send_two_factor_authentication_code' do
let ( :instance ) { build_guest_user }
let ( :instance ) { build_guest_user }
it 'raises an error by default' do
it 'raises an error by default' do
expect { instance . send_two_factor_authentication_code } .
expect { instance . send_two_factor_authentication_code ( 123 ) } .
to raise_error ( NotImplementedError )
to raise_error ( NotImplementedError )
end
end
it 'is overrideable' do
it 'is overrideable' do
def instance . send_two_factor_authentication_code
def instance . send_two_factor_authentication_code ( code )
'Code sent'
'Code sent'
end
end
expect ( instance . send_two_factor_authentication_code ) . to eq ( 'Code sent' )
expect ( instance . send_two_factor_authentication_code ( 123 ) ) . to eq ( 'Code sent' )
end
end
end
end
describe '#provisioning_uri' do
describe '#provisioning_uri' do
shared_examples 'provisioning_uri' do | instance |
shared_examples 'provisioning_uri' do | instance |
before do
it 'fails until generate_totp_secret is called' do
instance . email = 'houdini@example.com'
expect { instance . provisioning_uri } . to raise_error ( Exception )
instance . run_callbacks :create
end
end
it " returns uri with user's email " do
describe 'with secret set' do
expect ( instance . provisioning_uri ) .
before do
to match ( %r{ otpauth://totp/houdini@example.com \ ?secret= \ w { 16 } } )
instance . email = 'houdini@example.com'
end
instance . generate_totp_secret
end
it 'returns uri with issuer option' do
it " returns uri with user's email " do
expect ( instance . provisioning_uri ( 'houdini' ) ) .
expect ( instance . provisioning_uri ) .
to match ( %r{ otpauth://totp/houdini \ ?secret= \ w { 16 } $ } )
to match ( %r{ otpauth://totp/houdini@example.com \ ?secret= \ w { 16 } } )
end
end
it 'returns uri with issuer option' do
it 'returns uri with issuer option' do
require 'cgi'
expect ( instance . provisioning_uri ( 'houdini' ) ) .
to match ( %r{ otpauth://totp/houdini \ ?secret= \ w { 16 } $ } )
end
uri = URI . parse ( instance . provisioning_uri ( 'houdini' , issuer : 'Magic' ) )
it 'returns uri with issuer option' do
params = CGI . parse ( uri . query )
require 'cgi'
uri = URI . parse ( instance . provisioning_uri ( 'houdini' , issuer : 'Magic' ) )
params = CGI . parse ( uri . query )
expect ( uri . scheme ) . to eq ( 'otpauth' )
expect ( uri . scheme ) . to eq ( 'otpauth' )
expect ( uri . host ) . to eq ( 'totp' )
expect ( uri . host ) . to eq ( 'totp' )
expect ( uri . path ) . to eq ( '/Magic:houdini' )
expect ( uri . path ) . to eq ( '/Magic:houdini' )
expect ( params [ 'issuer' ] . shift ) . to eq ( 'Magic' )
expect ( params [ 'issuer' ] . shift ) . to eq ( 'Magic' )
expect ( params [ 'secret' ] . shift ) . to match ( / \ w{16} / )
expect ( params [ 'secret' ] . shift ) . to match ( / \ w{16} / )
end
end
end
end
end
@ -124,32 +153,22 @@ describe Devise::Models::TwoFactorAuthenticatable do
it_behaves_like 'provisioning_uri' , EncryptedUser . new
it_behaves_like 'provisioning_uri' , EncryptedUser . new
end
end
describe '#populate_otp_column ' do
describe '#generate_totp_secret ' do
shared_examples 'populate_otp_column ' do | klass |
shared_examples 'generate_totp_secret ' do | klass |
let ( :instance ) { klass . new }
let ( :instance ) { klass . new }
it 'populates otp_column on create' do
it 'populates otp_secret_key column' do
expect ( instance . otp_secret_key ) . to be_nil
# populate_otp_column called via before_create
instance . run_callbacks :create
expect ( instance . otp_secret_key ) . to match ( / \ w{16} / )
end
it 'repopulates otp_column' do
instance . run_callbacks :create
original_key = instance . otp_secret_key
original_key = instance . otp_secret_key
instance . populate_otp_column
instance . generate_totp_secret
expect ( instance . otp_secret_key ) . to match ( / \ w{16} / )
expect ( instance . otp_secret_key ) . to match ( / \ w{16} / )
expect ( instance . otp_secret_key ) . to_not eq ( original_key )
expect ( instance . otp_secret_key ) . to_not eq ( original_key )
end
end
end
end
it_behaves_like 'populate_otp_column ' , GuestUser
it_behaves_like 'generate_totp_secret' , GuestUser
it_behaves_like 'populate_otp_column ' , EncryptedUser
it_behaves_like 'generate_totp_secret ' , EncryptedUser
end
end
describe '#max_login_attempts' do
describe '#max_login_attempts' do