two_factor_authentication 1.1.3 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.codeclimate.yml +21 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +295 -0
- data/.travis.yml +14 -7
- data/CHANGELOG.md +119 -0
- data/Gemfile +12 -3
- data/README.md +320 -58
- data/app/controllers/devise/two_factor_authentication_controller.rb +65 -25
- data/app/views/devise/two_factor_authentication/show.html.erb +11 -2
- data/config/locales/en.yml +1 -0
- data/config/locales/es.yml +8 -0
- data/config/locales/fr.yml +8 -0
- data/config/locales/ru.yml +1 -0
- data/lib/generators/active_record/templates/migration.rb +9 -11
- data/lib/two_factor_authentication/controllers/helpers.rb +3 -3
- data/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb +12 -2
- data/lib/two_factor_authentication/models/two_factor_authenticatable.rb +158 -29
- data/lib/two_factor_authentication/orm/active_record.rb +2 -0
- data/lib/two_factor_authentication/routes.rb +3 -1
- data/lib/two_factor_authentication/schema.rb +24 -4
- data/lib/two_factor_authentication/version.rb +1 -1
- data/lib/two_factor_authentication.rb +20 -3
- data/spec/controllers/two_factor_authentication_controller_spec.rb +41 -0
- data/spec/features/two_factor_authenticatable_spec.rb +179 -30
- data/spec/generators/active_record/two_factor_authentication_generator_spec.rb +36 -0
- data/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb +272 -114
- data/spec/rails_app/app/controllers/home_controller.rb +1 -1
- data/spec/rails_app/app/models/admin.rb +6 -0
- data/spec/rails_app/app/models/encrypted_user.rb +15 -0
- data/spec/rails_app/app/models/guest_user.rb +8 -1
- data/spec/rails_app/app/models/user.rb +3 -4
- data/spec/rails_app/config/environments/test.rb +10 -1
- data/spec/rails_app/config/initializers/devise.rb +5 -3
- data/spec/rails_app/config/routes.rb +1 -0
- data/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb +2 -2
- data/spec/rails_app/db/migrate/20140407172619_two_factor_authentication_add_to_users.rb +1 -1
- data/spec/rails_app/db/migrate/20140407215513_add_nickanme_to_users.rb +1 -1
- data/spec/rails_app/db/migrate/20151224171231_add_encrypted_columns_to_user.rb +9 -0
- data/spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb +19 -0
- data/spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb +5 -0
- data/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb +42 -0
- data/spec/rails_app/db/schema.rb +35 -18
- data/spec/spec_helper.rb +4 -0
- data/spec/support/authenticated_model_helper.rb +33 -2
- data/spec/support/controller_helper.rb +16 -0
- data/spec/support/features_spec_helper.rb +24 -1
- data/spec/support/totp_helper.rb +11 -0
- data/two_factor_authentication.gemspec +4 -2
- metadata +133 -30
- data/spec/controllers/two_factor_auth_spec.rb +0 -18
@@ -1,7 +1,17 @@
|
|
1
1
|
Warden::Manager.after_authentication do |user, auth, options|
|
2
|
-
if
|
2
|
+
if auth.env["action_dispatch.cookies"]
|
3
|
+
expected_cookie_value = "#{user.class}-#{user.public_send(Devise.second_factor_resource_id)}"
|
4
|
+
actual_cookie_value = auth.env["action_dispatch.cookies"].signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME]
|
5
|
+
bypass_by_cookie = actual_cookie_value == expected_cookie_value
|
6
|
+
end
|
7
|
+
|
8
|
+
if user.respond_to?(:need_two_factor_authentication?) && !bypass_by_cookie
|
3
9
|
if auth.session(options[:scope])[TwoFactorAuthentication::NEED_AUTHENTICATION] = user.need_two_factor_authentication?(auth.request)
|
4
|
-
user.
|
10
|
+
user.send_new_otp if user.send_new_otp_after_login?
|
5
11
|
end
|
6
12
|
end
|
7
13
|
end
|
14
|
+
|
15
|
+
Warden::Manager.before_logout do |user, auth, _options|
|
16
|
+
auth.cookies.delete TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME if Devise.delete_cookie_on_logout
|
17
|
+
end
|
@@ -1,4 +1,7 @@
|
|
1
1
|
require 'two_factor_authentication/hooks/two_factor_authenticatable'
|
2
|
+
require 'rotp'
|
3
|
+
require 'encryptor'
|
4
|
+
|
2
5
|
module Devise
|
3
6
|
module Models
|
4
7
|
module TwoFactorAuthenticatable
|
@@ -6,53 +9,67 @@ module Devise
|
|
6
9
|
|
7
10
|
module ClassMethods
|
8
11
|
def has_one_time_password(options = {})
|
9
|
-
|
10
|
-
cattr_accessor :otp_column_name
|
11
|
-
self.otp_column_name = "otp_secret_key"
|
12
|
-
|
13
12
|
include InstanceMethodsOnActivation
|
14
|
-
|
15
|
-
before_create { populate_otp_column }
|
16
|
-
|
17
|
-
if respond_to?(:attributes_protected_by_default)
|
18
|
-
def self.attributes_protected_by_default #:nodoc:
|
19
|
-
super + [self.otp_column_name]
|
20
|
-
end
|
21
|
-
end
|
13
|
+
include EncryptionInstanceMethods if options[:encrypted] == true
|
22
14
|
end
|
23
|
-
|
15
|
+
|
16
|
+
::Devise::Models.config(
|
17
|
+
self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length,
|
18
|
+
:remember_otp_session_for_seconds, :otp_secret_encryption_key,
|
19
|
+
:direct_otp_length, :direct_otp_valid_for, :totp_timestamp, :delete_cookie_on_logout
|
20
|
+
)
|
24
21
|
end
|
25
22
|
|
26
23
|
module InstanceMethodsOnActivation
|
27
24
|
def authenticate_otp(code, options = {})
|
28
|
-
|
29
|
-
|
25
|
+
return true if direct_otp && authenticate_direct_otp(code)
|
26
|
+
return true if totp_enabled? && authenticate_totp(code, options)
|
27
|
+
false
|
28
|
+
end
|
30
29
|
|
31
|
-
|
30
|
+
def authenticate_direct_otp(code)
|
31
|
+
return false if direct_otp.nil? || direct_otp != code || direct_otp_expired?
|
32
|
+
clear_direct_otp
|
33
|
+
true
|
32
34
|
end
|
33
35
|
|
34
|
-
def
|
35
|
-
|
36
|
+
def authenticate_totp(code, options = {})
|
37
|
+
totp_secret = options[:otp_secret_key] || otp_secret_key
|
38
|
+
digits = options[:otp_length] || self.class.otp_length
|
39
|
+
drift = options[:drift] || self.class.allowed_otp_drift_seconds
|
40
|
+
raise "authenticate_totp called with no otp_secret_key set" if totp_secret.nil?
|
41
|
+
totp = ROTP::TOTP.new(totp_secret, digits: digits)
|
42
|
+
new_timestamp = totp.verify(
|
43
|
+
without_spaces(code),
|
44
|
+
drift_ahead: drift, drift_behind: drift, after: totp_timestamp
|
45
|
+
)
|
46
|
+
return false unless new_timestamp
|
47
|
+
self.totp_timestamp = new_timestamp
|
48
|
+
true
|
36
49
|
end
|
37
50
|
|
38
51
|
def provisioning_uri(account = nil, options = {})
|
39
|
-
|
40
|
-
|
52
|
+
totp_secret = options[:otp_secret_key] || otp_secret_key
|
53
|
+
options[:digits] ||= options[:otp_length] || self.class.otp_length
|
54
|
+
raise "provisioning_uri called with no otp_secret_key set" if totp_secret.nil?
|
55
|
+
account ||= email if respond_to?(:email)
|
56
|
+
ROTP::TOTP.new(totp_secret, options).provisioning_uri(account)
|
41
57
|
end
|
42
58
|
|
43
|
-
def
|
44
|
-
|
59
|
+
def need_two_factor_authentication?(request)
|
60
|
+
true
|
45
61
|
end
|
46
62
|
|
47
|
-
def
|
48
|
-
|
63
|
+
def send_new_otp(options = {})
|
64
|
+
create_direct_otp options
|
65
|
+
send_two_factor_authentication_code(direct_otp)
|
49
66
|
end
|
50
67
|
|
51
|
-
def
|
52
|
-
|
68
|
+
def send_new_otp_after_login?
|
69
|
+
!totp_enabled?
|
53
70
|
end
|
54
71
|
|
55
|
-
def send_two_factor_authentication_code
|
72
|
+
def send_two_factor_authentication_code(code)
|
56
73
|
raise NotImplementedError.new("No default implementation - please define in your class.")
|
57
74
|
end
|
58
75
|
|
@@ -64,10 +81,122 @@ module Devise
|
|
64
81
|
self.class.max_login_attempts
|
65
82
|
end
|
66
83
|
|
67
|
-
def
|
68
|
-
|
84
|
+
def totp_enabled?
|
85
|
+
respond_to?(:otp_secret_key) && !otp_secret_key.nil?
|
69
86
|
end
|
70
87
|
|
88
|
+
def confirm_totp_secret(secret, code, options = {})
|
89
|
+
return false unless authenticate_totp(code, {otp_secret_key: secret})
|
90
|
+
self.otp_secret_key = secret
|
91
|
+
true
|
92
|
+
end
|
93
|
+
|
94
|
+
def generate_totp_secret
|
95
|
+
ROTP::Base32.random_base32
|
96
|
+
end
|
97
|
+
|
98
|
+
def create_direct_otp(options = {})
|
99
|
+
# Create a new random OTP and store it in the database
|
100
|
+
digits = options[:length] || self.class.direct_otp_length || 6
|
101
|
+
update_attributes(
|
102
|
+
direct_otp: random_base10(digits),
|
103
|
+
direct_otp_sent_at: Time.now.utc
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def without_spaces(code)
|
110
|
+
code.gsub(/\s/, '')
|
111
|
+
end
|
112
|
+
|
113
|
+
def random_base10(digits)
|
114
|
+
SecureRandom.random_number(10**digits).to_s.rjust(digits, '0')
|
115
|
+
end
|
116
|
+
|
117
|
+
def direct_otp_expired?
|
118
|
+
Time.now.utc > direct_otp_sent_at + self.class.direct_otp_valid_for
|
119
|
+
end
|
120
|
+
|
121
|
+
def clear_direct_otp
|
122
|
+
update_attributes(direct_otp: nil, direct_otp_sent_at: nil)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
module EncryptionInstanceMethods
|
127
|
+
def otp_secret_key
|
128
|
+
otp_decrypt(encrypted_otp_secret_key)
|
129
|
+
end
|
130
|
+
|
131
|
+
def otp_secret_key=(value)
|
132
|
+
self.encrypted_otp_secret_key = otp_encrypt(value)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def otp_decrypt(encrypted_value)
|
138
|
+
return encrypted_value if encrypted_value.blank?
|
139
|
+
|
140
|
+
encrypted_value = encrypted_value.unpack('m').first
|
141
|
+
|
142
|
+
value = ::Encryptor.decrypt(encryption_options_for(encrypted_value))
|
143
|
+
|
144
|
+
if defined?(Encoding)
|
145
|
+
encoding = Encoding.default_internal || Encoding.default_external
|
146
|
+
value = value.force_encoding(encoding.name)
|
147
|
+
end
|
148
|
+
|
149
|
+
value
|
150
|
+
end
|
151
|
+
|
152
|
+
def otp_encrypt(value)
|
153
|
+
return value if value.blank?
|
154
|
+
|
155
|
+
value = value.to_s
|
156
|
+
encrypted_value = ::Encryptor.encrypt(encryption_options_for(value))
|
157
|
+
|
158
|
+
encrypted_value = [encrypted_value].pack('m')
|
159
|
+
|
160
|
+
encrypted_value
|
161
|
+
end
|
162
|
+
|
163
|
+
def encryption_options_for(value)
|
164
|
+
{
|
165
|
+
value: value,
|
166
|
+
key: Devise.otp_secret_encryption_key,
|
167
|
+
iv: iv_for_attribute,
|
168
|
+
salt: salt_for_attribute,
|
169
|
+
algorithm: 'aes-256-cbc'
|
170
|
+
}
|
171
|
+
end
|
172
|
+
|
173
|
+
def iv_for_attribute(algorithm = 'aes-256-cbc')
|
174
|
+
iv = encrypted_otp_secret_key_iv
|
175
|
+
|
176
|
+
if iv.nil?
|
177
|
+
algo = OpenSSL::Cipher.new(algorithm)
|
178
|
+
iv = [algo.random_iv].pack('m')
|
179
|
+
self.encrypted_otp_secret_key_iv = iv
|
180
|
+
end
|
181
|
+
|
182
|
+
iv.unpack('m').first if iv.present?
|
183
|
+
end
|
184
|
+
|
185
|
+
def salt_for_attribute
|
186
|
+
salt = encrypted_otp_secret_key_salt ||
|
187
|
+
self.encrypted_otp_secret_key_salt = generate_random_base64_encoded_salt
|
188
|
+
|
189
|
+
decode_salt_if_encoded(salt)
|
190
|
+
end
|
191
|
+
|
192
|
+
def generate_random_base64_encoded_salt
|
193
|
+
prefix = '_'
|
194
|
+
prefix + [SecureRandom.random_bytes].pack('m')
|
195
|
+
end
|
196
|
+
|
197
|
+
def decode_salt_if_encoded(salt)
|
198
|
+
salt.slice(0).eql?('_') ? salt.slice(1..-1).unpack('m').first : salt
|
199
|
+
end
|
71
200
|
end
|
72
201
|
end
|
73
202
|
end
|
@@ -3,7 +3,9 @@ module ActionDispatch::Routing
|
|
3
3
|
protected
|
4
4
|
|
5
5
|
def devise_two_factor_authentication(mapping, controllers)
|
6
|
-
resource :two_factor_authentication, :only => [:show, :update], :path => mapping.path_names[:two_factor_authentication], :controller => controllers[:two_factor_authentication]
|
6
|
+
resource :two_factor_authentication, :only => [:show, :update, :resend_code], :path => mapping.path_names[:two_factor_authentication], :controller => controllers[:two_factor_authentication] do
|
7
|
+
collection { get "resend_code" }
|
8
|
+
end
|
7
9
|
end
|
8
10
|
end
|
9
11
|
end
|
@@ -1,11 +1,31 @@
|
|
1
1
|
module TwoFactorAuthentication
|
2
2
|
module Schema
|
3
|
-
def otp_secret_key
|
4
|
-
apply_devise_schema :otp_secret_key, String
|
5
|
-
end
|
6
|
-
|
7
3
|
def second_factor_attempts_count
|
8
4
|
apply_devise_schema :second_factor_attempts_count, Integer, :default => 0
|
9
5
|
end
|
6
|
+
|
7
|
+
def encrypted_otp_secret_key
|
8
|
+
apply_devise_schema :encrypted_otp_secret_key, String
|
9
|
+
end
|
10
|
+
|
11
|
+
def encrypted_otp_secret_key_iv
|
12
|
+
apply_devise_schema :encrypted_otp_secret_key_iv, String
|
13
|
+
end
|
14
|
+
|
15
|
+
def encrypted_otp_secret_key_salt
|
16
|
+
apply_devise_schema :encrypted_otp_secret_key_salt, String
|
17
|
+
end
|
18
|
+
|
19
|
+
def direct_otp
|
20
|
+
apply_devise_schema :direct_otp, String
|
21
|
+
end
|
22
|
+
|
23
|
+
def direct_otp_sent_at
|
24
|
+
apply_devise_schema :direct_otp_sent_at, DateTime
|
25
|
+
end
|
26
|
+
|
27
|
+
def totp_timestamp
|
28
|
+
apply_devise_schema :totp_timestamp, Timestamp
|
29
|
+
end
|
10
30
|
end
|
11
31
|
end
|
@@ -2,10 +2,8 @@ require 'two_factor_authentication/version'
|
|
2
2
|
require 'devise'
|
3
3
|
require 'active_support/concern'
|
4
4
|
require "active_model"
|
5
|
-
require "active_record"
|
6
5
|
require "active_support/core_ext/class/attribute_accessors"
|
7
6
|
require "cgi"
|
8
|
-
require "rotp"
|
9
7
|
|
10
8
|
module Devise
|
11
9
|
mattr_accessor :max_login_attempts
|
@@ -16,10 +14,29 @@ module Devise
|
|
16
14
|
|
17
15
|
mattr_accessor :otp_length
|
18
16
|
@@otp_length = 6
|
17
|
+
|
18
|
+
mattr_accessor :direct_otp_length
|
19
|
+
@@direct_otp_length = 6
|
20
|
+
|
21
|
+
mattr_accessor :direct_otp_valid_for
|
22
|
+
@@direct_otp_valid_for = 5.minutes
|
23
|
+
|
24
|
+
mattr_accessor :remember_otp_session_for_seconds
|
25
|
+
@@remember_otp_session_for_seconds = 0
|
26
|
+
|
27
|
+
mattr_accessor :otp_secret_encryption_key
|
28
|
+
@@otp_secret_encryption_key = ''
|
29
|
+
|
30
|
+
mattr_accessor :second_factor_resource_id
|
31
|
+
@@second_factor_resource_id = 'id'
|
32
|
+
|
33
|
+
mattr_accessor :delete_cookie_on_logout
|
34
|
+
@@delete_cookie_on_logout = false
|
19
35
|
end
|
20
36
|
|
21
37
|
module TwoFactorAuthentication
|
22
38
|
NEED_AUTHENTICATION = 'need_two_factor_authentication'
|
39
|
+
REMEMBER_TFA_COOKIE_NAME = "remember_tfa"
|
23
40
|
|
24
41
|
autoload :Schema, 'two_factor_authentication/schema'
|
25
42
|
module Controllers
|
@@ -29,7 +46,7 @@ end
|
|
29
46
|
|
30
47
|
Devise.add_module :two_factor_authenticatable, :model => 'two_factor_authentication/models/two_factor_authenticatable', :controller => :two_factor_authentication, :route => :two_factor_authentication
|
31
48
|
|
32
|
-
require 'two_factor_authentication/orm/active_record'
|
49
|
+
require 'two_factor_authentication/orm/active_record' if defined?(ActiveRecord::Base)
|
33
50
|
require 'two_factor_authentication/routes'
|
34
51
|
require 'two_factor_authentication/models/two_factor_authenticatable'
|
35
52
|
require 'two_factor_authentication/rails'
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Devise::TwoFactorAuthenticationController, type: :controller do
|
4
|
+
describe 'is_fully_authenticated? helper' do
|
5
|
+
def post_code(code)
|
6
|
+
if Rails::VERSION::MAJOR >= 5
|
7
|
+
post :update, params: { code: code }
|
8
|
+
else
|
9
|
+
post :update, code: code
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
before do
|
14
|
+
sign_in
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'after user enters valid OTP code' do
|
18
|
+
it 'returns true' do
|
19
|
+
controller.current_user.send_new_otp
|
20
|
+
post_code controller.current_user.direct_otp
|
21
|
+
expect(subject.is_fully_authenticated?).to eq true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when user has not entered any OTP yet' do
|
26
|
+
it 'returns false' do
|
27
|
+
get :show
|
28
|
+
|
29
|
+
expect(subject.is_fully_authenticated?).to eq false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'when user enters an invalid OTP' do
|
34
|
+
it 'returns false' do
|
35
|
+
post_code '12345'
|
36
|
+
|
37
|
+
expect(subject.is_fully_authenticated?).to eq false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,47 +1,65 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
include AuthenticatedModelHelper
|
2
3
|
|
3
4
|
feature "User of two factor authentication" do
|
4
|
-
|
5
|
+
context 'sending two factor authentication code via SMS' do
|
6
|
+
shared_examples 'sends and authenticates code' do |user, type|
|
7
|
+
before do
|
8
|
+
user.reload
|
9
|
+
if type == 'encrypted'
|
10
|
+
allow(User).to receive(:has_one_time_password).with(encrypted: true)
|
11
|
+
end
|
12
|
+
end
|
5
13
|
|
6
|
-
|
7
|
-
|
14
|
+
it 'does not send an SMS before the user has signed in' do
|
15
|
+
expect(SMSProvider.messages).to be_empty
|
16
|
+
end
|
8
17
|
|
9
|
-
|
10
|
-
|
11
|
-
|
18
|
+
it 'sends code via SMS after sign in' do
|
19
|
+
visit new_user_session_path
|
20
|
+
complete_sign_in_form_for(user)
|
12
21
|
|
13
|
-
|
14
|
-
expect(SMSProvider.messages).to be_empty
|
22
|
+
expect(page).to have_content 'Enter the code that was sent to you'
|
15
23
|
|
16
|
-
|
17
|
-
|
24
|
+
expect(SMSProvider.messages.size).to eq(1)
|
25
|
+
message = SMSProvider.last_message
|
26
|
+
expect(message.to).to eq(user.phone_number)
|
27
|
+
expect(message.body).to eq(user.reload.direct_otp)
|
28
|
+
end
|
18
29
|
|
19
|
-
|
30
|
+
it 'authenticates a valid OTP code' do
|
31
|
+
visit new_user_session_path
|
32
|
+
complete_sign_in_form_for(user)
|
20
33
|
|
21
|
-
|
22
|
-
message = SMSProvider.last_message
|
23
|
-
expect(message.to).to eq(user.phone_number)
|
24
|
-
expect(message.body).to eq(user.otp_code)
|
25
|
-
end
|
34
|
+
expect(page).to have_content('You are signed in as Marissa')
|
26
35
|
|
27
|
-
|
36
|
+
fill_in 'code', with: SMSProvider.last_message.body
|
37
|
+
click_button 'Submit'
|
28
38
|
|
29
|
-
|
30
|
-
|
39
|
+
within('.flash.notice') do
|
40
|
+
expect(page).to have_content('Two factor authentication successful.')
|
41
|
+
end
|
42
|
+
|
43
|
+
expect(current_path).to eq root_path
|
44
|
+
end
|
31
45
|
end
|
32
46
|
|
33
|
-
|
34
|
-
|
47
|
+
it_behaves_like 'sends and authenticates code', create_user('not_encrypted')
|
48
|
+
it_behaves_like 'sends and authenticates code', create_user, 'encrypted'
|
49
|
+
end
|
35
50
|
|
36
|
-
|
37
|
-
|
51
|
+
scenario "must be logged in" do
|
52
|
+
visit user_two_factor_authentication_path
|
38
53
|
|
39
|
-
|
40
|
-
|
54
|
+
expect(page).to have_content("Welcome Home")
|
55
|
+
expect(page).to have_content("You are signed out")
|
56
|
+
end
|
41
57
|
|
42
|
-
|
43
|
-
|
44
|
-
|
58
|
+
context "when logged in" do
|
59
|
+
let(:user) { create_user }
|
60
|
+
|
61
|
+
background do
|
62
|
+
login_as user
|
45
63
|
end
|
46
64
|
|
47
65
|
scenario "is redirected to TFA when path requires authentication" do
|
@@ -49,7 +67,7 @@ feature "User of two factor authentication" do
|
|
49
67
|
|
50
68
|
expect(page).to_not have_content("Your Personal Dashboard")
|
51
69
|
|
52
|
-
fill_in "code", with:
|
70
|
+
fill_in "code", with: SMSProvider.last_message.body
|
53
71
|
click_button "Submit"
|
54
72
|
|
55
73
|
expect(page).to have_content("Your Personal Dashboard")
|
@@ -67,7 +85,7 @@ feature "User of two factor authentication" do
|
|
67
85
|
fill_in "code", with: "incorrect#{rand(100)}"
|
68
86
|
click_button "Submit"
|
69
87
|
|
70
|
-
within(".flash.
|
88
|
+
within(".flash.alert") do
|
71
89
|
expect(page).to have_content("Attempt failed")
|
72
90
|
end
|
73
91
|
end
|
@@ -84,5 +102,136 @@ feature "User of two factor authentication" do
|
|
84
102
|
expect(page).to have_content("Access completely denied")
|
85
103
|
expect(page).to have_content("You are signed out")
|
86
104
|
end
|
105
|
+
|
106
|
+
describe "rememberable TFA" do
|
107
|
+
before do
|
108
|
+
@original_remember_otp_session_for_seconds = User.remember_otp_session_for_seconds
|
109
|
+
User.remember_otp_session_for_seconds = 30.days
|
110
|
+
end
|
111
|
+
|
112
|
+
after do
|
113
|
+
User.remember_otp_session_for_seconds = @original_remember_otp_session_for_seconds
|
114
|
+
end
|
115
|
+
|
116
|
+
scenario "doesn't require TFA code again within 30 days" do
|
117
|
+
sms_sign_in
|
118
|
+
|
119
|
+
logout
|
120
|
+
|
121
|
+
login_as user
|
122
|
+
visit dashboard_path
|
123
|
+
expect(page).to have_content("Your Personal Dashboard")
|
124
|
+
expect(page).to have_content("You are signed in as Marissa")
|
125
|
+
end
|
126
|
+
|
127
|
+
scenario "requires TFA code again after 30 days" do
|
128
|
+
sms_sign_in
|
129
|
+
|
130
|
+
logout
|
131
|
+
|
132
|
+
Timecop.travel(30.days.from_now)
|
133
|
+
login_as user
|
134
|
+
visit dashboard_path
|
135
|
+
expect(page).to have_content("You are signed in as Marissa")
|
136
|
+
expect(page).to have_content("Enter the code that was sent to you")
|
137
|
+
end
|
138
|
+
|
139
|
+
scenario 'TFA should be different for different users' do
|
140
|
+
sms_sign_in
|
141
|
+
|
142
|
+
tfa_cookie1 = get_tfa_cookie()
|
143
|
+
|
144
|
+
logout
|
145
|
+
reset_session!
|
146
|
+
|
147
|
+
user2 = create_user()
|
148
|
+
login_as(user2)
|
149
|
+
sms_sign_in
|
150
|
+
|
151
|
+
tfa_cookie2 = get_tfa_cookie()
|
152
|
+
|
153
|
+
expect(tfa_cookie1).not_to eq tfa_cookie2
|
154
|
+
end
|
155
|
+
|
156
|
+
def sms_sign_in
|
157
|
+
SMSProvider.messages.clear()
|
158
|
+
visit user_two_factor_authentication_path
|
159
|
+
fill_in 'code', with: SMSProvider.last_message.body
|
160
|
+
click_button 'Submit'
|
161
|
+
end
|
162
|
+
|
163
|
+
scenario 'TFA should be unique for specific user' do
|
164
|
+
sms_sign_in
|
165
|
+
|
166
|
+
tfa_cookie1 = get_tfa_cookie()
|
167
|
+
|
168
|
+
logout
|
169
|
+
reset_session!
|
170
|
+
|
171
|
+
user2 = create_user()
|
172
|
+
set_tfa_cookie(tfa_cookie1)
|
173
|
+
login_as(user2)
|
174
|
+
visit dashboard_path
|
175
|
+
expect(page).to have_content("Enter the code that was sent to you")
|
176
|
+
end
|
177
|
+
|
178
|
+
scenario 'Delete cookie when user logs out if enabled' do
|
179
|
+
user.class.delete_cookie_on_logout = true
|
180
|
+
|
181
|
+
login_as user
|
182
|
+
logout
|
183
|
+
|
184
|
+
login_as user
|
185
|
+
|
186
|
+
visit dashboard_path
|
187
|
+
expect(page).to have_content("Enter the code that was sent to you")
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'sets the warden session need_two_factor_authentication key to true' do
|
192
|
+
session_hash = { 'need_two_factor_authentication' => true }
|
193
|
+
|
194
|
+
expect(page.get_rack_session_key('warden.user.user.session')).to eq session_hash
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
describe 'signing in' do
|
199
|
+
let(:user) { create_user }
|
200
|
+
let(:admin) { create_admin }
|
201
|
+
|
202
|
+
scenario 'user signs is' do
|
203
|
+
visit new_user_session_path
|
204
|
+
complete_sign_in_form_for(user)
|
205
|
+
|
206
|
+
expect(page).to have_content('Signed in successfully.')
|
207
|
+
end
|
208
|
+
|
209
|
+
scenario 'admin signs in' do
|
210
|
+
visit new_admin_session_path
|
211
|
+
complete_sign_in_form_for(admin)
|
212
|
+
|
213
|
+
expect(page).to have_content('Signed in successfully.')
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
describe 'signing out' do
|
218
|
+
let(:user) { create_user }
|
219
|
+
let(:admin) { create_admin }
|
220
|
+
|
221
|
+
scenario 'user signs out' do
|
222
|
+
visit new_user_session_path
|
223
|
+
complete_sign_in_form_for(user)
|
224
|
+
visit destroy_user_session_path
|
225
|
+
|
226
|
+
expect(page).to have_content('Signed out successfully.')
|
227
|
+
end
|
228
|
+
|
229
|
+
scenario 'admin signs out' do
|
230
|
+
visit new_admin_session_path
|
231
|
+
complete_sign_in_form_for(admin)
|
232
|
+
visit destroy_admin_session_path
|
233
|
+
|
234
|
+
expect(page).to have_content('Signed out successfully.')
|
235
|
+
end
|
87
236
|
end
|
88
237
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'generators/active_record/two_factor_authentication_generator'
|
4
|
+
|
5
|
+
describe ActiveRecord::Generators::TwoFactorAuthenticationGenerator, type: :generator do
|
6
|
+
destination File.expand_path('../../../../../tmp', __FILE__)
|
7
|
+
|
8
|
+
before do
|
9
|
+
prepare_destination
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'runs all methods in the generator' do
|
13
|
+
gen = generator %w(users)
|
14
|
+
expect(gen).to receive(:copy_two_factor_authentication_migration)
|
15
|
+
gen.invoke_all
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'the generated files' do
|
19
|
+
before do
|
20
|
+
run_generator %w(users)
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'the migration' do
|
24
|
+
subject { migration_file('db/migrate/two_factor_authentication_add_to_users.rb') }
|
25
|
+
|
26
|
+
it { is_expected.to exist }
|
27
|
+
it { is_expected.to be_a_migration }
|
28
|
+
it { is_expected.to contain /def change/ }
|
29
|
+
it { is_expected.to contain /add_column :users, :second_factor_attempts_count, :integer, default: 0/ }
|
30
|
+
it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key, :string/ }
|
31
|
+
it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key_iv, :string/ }
|
32
|
+
it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key_salt, :string/ }
|
33
|
+
it { is_expected.to contain /add_index :users, :encrypted_otp_secret_key, unique: true/ }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|