two_factor_authentication 2.1.1 → 3.0.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 +5 -5
- data/.github/workflows/ci.yml +77 -0
- data/.gitignore +3 -0
- data/CHANGELOG.md +37 -2
- data/Gemfile +29 -9
- data/MIGRATION_GUIDE.md +143 -0
- data/README.md +217 -28
- data/app/controllers/devise/two_factor_authentication_controller.rb +3 -2
- data/app/views/devise/two_factor_authentication/show.html.erb +4 -4
- data/config/locales/de.yml +8 -0
- data/lib/generators/active_record/templates/migration.rb +9 -1
- data/lib/generators/active_record/two_factor_authentication_generator.rb +18 -0
- data/lib/generators/two_factor_authentication/two_factor_authentication_generator.rb +1 -1
- data/lib/two_factor_authentication/controllers/helpers.rb +8 -4
- data/lib/two_factor_authentication/models/two_factor_authenticatable.rb +16 -6
- data/lib/two_factor_authentication/orm/active_record.rb +2 -0
- data/lib/two_factor_authentication/routes.rb +1 -1
- data/lib/two_factor_authentication/schema.rb +7 -7
- data/lib/two_factor_authentication/version.rb +1 -1
- data/lib/two_factor_authentication.rb +1 -2
- data/spec/controllers/two_factor_authentication_controller_spec.rb +87 -2
- data/spec/features/two_factor_authenticatable_spec.rb +119 -14
- data/spec/generators/active_record/two_factor_authentication_generator_spec.rb +52 -4
- data/spec/generators/two_factor_authentication/two_factor_authentication_generator_spec.rb +37 -0
- data/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb +97 -10
- data/spec/lib/two_factor_authentication/schema_spec.rb +41 -0
- data/spec/rails_app/app/assets/config/manifest.js +2 -0
- data/spec/rails_app/app/controllers/home_controller.rb +6 -1
- data/spec/rails_app/app/models/guest_user.rb +1 -1
- data/spec/rails_app/app/models/secure_user.rb +13 -0
- data/spec/rails_app/config/application.rb +1 -1
- data/spec/rails_app/config/environments/test.rb +3 -8
- data/spec/rails_app/config/initializers/inflections.rb +4 -0
- data/spec/rails_app/config/routes.rb +2 -0
- data/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb +1 -1
- 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 +1 -1
- data/spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb +1 -1
- data/spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb +1 -1
- data/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb +1 -1
- data/spec/rails_app/db/migrate/20260929000000_add_otp_columns_to_users.rb +8 -0
- data/spec/rails_app/db/migrate/20260929000001_devise_create_secure_users.rb +22 -0
- data/spec/rails_app/db/schema.rb +51 -36
- data/spec/rails_app/lib/sms_provider.rb +2 -4
- data/spec/requests/two_factor_authentication_helpers_spec.rb +48 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/authenticated_model_helper.rb +5 -30
- data/spec/support/controller_helper.rb +5 -4
- data/spec/support/features_spec_helper.rb +1 -0
- data/spec/support/totp_helper.rb +1 -1
- data/two_factor_authentication.gemspec +5 -6
- metadata +45 -28
- data/.travis.yml +0 -29
- data/spec/rails_app/config/initializers/secret_token.rb +0 -7
|
@@ -86,6 +86,11 @@ describe Devise::Models::TwoFactorAuthenticatable do
|
|
|
86
86
|
expect(do_invoke(code, instance)).to eq(true)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
it 'authenticates a code entered with a space' do
|
|
90
|
+
code = @totp_helper.totp_code.insert(3, ' ')
|
|
91
|
+
expect(do_invoke(code, instance)).to eq(true)
|
|
92
|
+
end
|
|
93
|
+
|
|
89
94
|
it 'does not authenticate an old code' do
|
|
90
95
|
code = @totp_helper.totp_code(1.minutes.ago.to_i)
|
|
91
96
|
expect(do_invoke(code, instance)).to eq(false)
|
|
@@ -132,25 +137,29 @@ describe Devise::Models::TwoFactorAuthenticatable do
|
|
|
132
137
|
end
|
|
133
138
|
|
|
134
139
|
it "returns uri with user's email" do
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
uri = URI.parse(instance.provisioning_uri)
|
|
141
|
+
params = URI.decode_www_form(uri.query).to_h
|
|
142
|
+
|
|
143
|
+
expect(uri.scheme).to eq('otpauth')
|
|
144
|
+
expect(uri.host).to eq('totp')
|
|
145
|
+
expect(URI.decode_www_form_component(uri.path)).to eq('/houdini@example.com')
|
|
146
|
+
expect(params['secret']).to match(/\w{32}/)
|
|
137
147
|
end
|
|
138
148
|
|
|
139
149
|
it 'returns uri with issuer option' do
|
|
140
150
|
expect(instance.provisioning_uri('houdini')).
|
|
141
|
-
to match(%r{otpauth://totp/houdini\?secret=\w{
|
|
151
|
+
to match(%r{otpauth://totp/houdini\?secret=\w{32}$})
|
|
142
152
|
end
|
|
143
153
|
|
|
144
154
|
it 'returns uri with issuer option' do
|
|
145
|
-
require 'cgi'
|
|
146
155
|
uri = URI.parse(instance.provisioning_uri('houdini', issuer: 'Magic'))
|
|
147
|
-
params =
|
|
156
|
+
params = URI.decode_www_form(uri.query).to_h
|
|
148
157
|
|
|
149
158
|
expect(uri.scheme).to eq('otpauth')
|
|
150
159
|
expect(uri.host).to eq('totp')
|
|
151
|
-
expect(uri.path).to eq('/Magic:houdini')
|
|
152
|
-
expect(params['issuer']
|
|
153
|
-
expect(params['secret']
|
|
160
|
+
expect(URI.decode_www_form_component(uri.path)).to eq('/Magic:houdini')
|
|
161
|
+
expect(params['issuer']).to eq('Magic')
|
|
162
|
+
expect(params['secret']).to match(/\w{32}/)
|
|
154
163
|
end
|
|
155
164
|
end
|
|
156
165
|
end
|
|
@@ -163,10 +172,10 @@ describe Devise::Models::TwoFactorAuthenticatable do
|
|
|
163
172
|
shared_examples 'generate_totp_secret' do |klass|
|
|
164
173
|
let(:instance) { klass.new }
|
|
165
174
|
|
|
166
|
-
it 'returns a
|
|
175
|
+
it 'returns a 32 character string' do
|
|
167
176
|
secret = instance.generate_totp_secret
|
|
168
177
|
|
|
169
|
-
expect(secret).to match(/\w{
|
|
178
|
+
expect(secret).to match(/\w{32}/)
|
|
170
179
|
end
|
|
171
180
|
end
|
|
172
181
|
|
|
@@ -318,4 +327,82 @@ describe Devise::Models::TwoFactorAuthenticatable do
|
|
|
318
327
|
end
|
|
319
328
|
end
|
|
320
329
|
end
|
|
330
|
+
|
|
331
|
+
context 'with an ActiveRecord model' do
|
|
332
|
+
let(:user) { create_user }
|
|
333
|
+
|
|
334
|
+
describe '#create_direct_otp' do
|
|
335
|
+
it 'persists the code and the time it was sent' do
|
|
336
|
+
Timecop.freeze do
|
|
337
|
+
user.create_direct_otp
|
|
338
|
+
user.reload
|
|
339
|
+
|
|
340
|
+
expect(user.direct_otp).to match(/\A\d{6}\z/)
|
|
341
|
+
expect(user.direct_otp_sent_at).to be_within(1.second).of(Time.now)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
describe '#authenticate_direct_otp' do
|
|
347
|
+
it 'clears the persisted code after a successful match' do
|
|
348
|
+
user.create_direct_otp
|
|
349
|
+
code = user.reload.direct_otp
|
|
350
|
+
|
|
351
|
+
expect(user.authenticate_direct_otp(code)).to eq(true)
|
|
352
|
+
user.reload
|
|
353
|
+
expect(user.direct_otp).to be_nil
|
|
354
|
+
expect(user.direct_otp_sent_at).to be_nil
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
describe '#send_new_otp' do
|
|
359
|
+
it 'delivers the persisted code' do
|
|
360
|
+
user.send_new_otp
|
|
361
|
+
|
|
362
|
+
expect(SMSProvider.last_message.body).to eq(user.reload.direct_otp)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
describe '#authenticate_totp' do
|
|
367
|
+
shared_examples 'persisted totp' do
|
|
368
|
+
let(:secret) { record.generate_totp_secret }
|
|
369
|
+
|
|
370
|
+
before do
|
|
371
|
+
record.otp_secret_key = secret
|
|
372
|
+
record.save!
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
it 'stores the last used time step as a time' do
|
|
376
|
+
expect(record.authenticate_totp(ROTP::TOTP.new(secret).now)).to eq(true)
|
|
377
|
+
record.save!
|
|
378
|
+
|
|
379
|
+
expect(record.reload.totp_timestamp).to be_a(Time)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
it 'rejects a reused code after the record is reloaded' do
|
|
383
|
+
code = ROTP::TOTP.new(secret).now
|
|
384
|
+
expect(record.authenticate_totp(code)).to eq(true)
|
|
385
|
+
record.save!
|
|
386
|
+
|
|
387
|
+
expect(record.class.find(record.id).authenticate_totp(code)).to eq(false)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
context 'with a plain secret' do
|
|
392
|
+
let(:record) { user }
|
|
393
|
+
include_examples 'persisted totp'
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
context 'with an encrypted secret' do
|
|
397
|
+
let(:record) { create_secure_user }
|
|
398
|
+
include_examples 'persisted totp'
|
|
399
|
+
|
|
400
|
+
it 'keeps the secret encrypted in the database' do
|
|
401
|
+
expect(record.reload.encrypted_otp_secret_key).to be_present
|
|
402
|
+
expect(record.encrypted_otp_secret_key).not_to include(secret)
|
|
403
|
+
expect(record.otp_secret_key).to eq(secret)
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
321
408
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe TwoFactorAuthentication::Schema do
|
|
4
|
+
let(:connection) { ActiveRecord::Base.connection }
|
|
5
|
+
|
|
6
|
+
after { connection.drop_table(:schema_helper_users, if_exists: true) }
|
|
7
|
+
|
|
8
|
+
def columns
|
|
9
|
+
connection.columns(:schema_helper_users).index_by(&:name)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'adds every column in create_table' do
|
|
13
|
+
connection.create_table(:schema_helper_users) do |t|
|
|
14
|
+
t.second_factor_attempts_count
|
|
15
|
+
t.encrypted_otp_secret_key
|
|
16
|
+
t.encrypted_otp_secret_key_iv
|
|
17
|
+
t.encrypted_otp_secret_key_salt
|
|
18
|
+
t.direct_otp
|
|
19
|
+
t.direct_otp_sent_at
|
|
20
|
+
t.totp_timestamp
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
expect(columns['second_factor_attempts_count'].type).to eq(:integer)
|
|
24
|
+
expect(columns['second_factor_attempts_count'].default.to_i).to eq(0)
|
|
25
|
+
%w(encrypted_otp_secret_key encrypted_otp_secret_key_iv encrypted_otp_secret_key_salt direct_otp).each do |name|
|
|
26
|
+
expect(columns[name].type).to eq(:string)
|
|
27
|
+
end
|
|
28
|
+
expect(columns['direct_otp_sent_at'].type).to eq(:datetime)
|
|
29
|
+
expect(columns['totp_timestamp'].type).to eq(:datetime)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'adds columns in change_table' do
|
|
33
|
+
connection.create_table(:schema_helper_users)
|
|
34
|
+
connection.change_table(:schema_helper_users) do |t|
|
|
35
|
+
t.second_factor_attempts_count
|
|
36
|
+
t.totp_timestamp
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
expect(columns.keys).to include('second_factor_attempts_count', 'totp_timestamp')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
class HomeController < ApplicationController
|
|
2
|
-
before_action :authenticate_user!, only: :dashboard
|
|
2
|
+
before_action :authenticate_user!, only: [:dashboard, :secret]
|
|
3
3
|
|
|
4
4
|
def index
|
|
5
5
|
end
|
|
@@ -7,4 +7,9 @@ class HomeController < ApplicationController
|
|
|
7
7
|
def dashboard
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
# Responds to any format, to prove the 2FA check halts non-HTML/JSON requests
|
|
11
|
+
def secret
|
|
12
|
+
render plain: 'top secret'
|
|
13
|
+
end
|
|
14
|
+
|
|
10
15
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class SecureUser < ActiveRecord::Base
|
|
2
|
+
devise :two_factor_authenticatable, :database_authenticatable, :validatable
|
|
3
|
+
|
|
4
|
+
has_one_time_password(encrypted: true)
|
|
5
|
+
|
|
6
|
+
def send_two_factor_authentication_code(code)
|
|
7
|
+
SMSProvider.send_message(to: phone_number, body: code)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def phone_number
|
|
11
|
+
'14155550000'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -9,14 +9,8 @@ Dummy::Application.configure do
|
|
|
9
9
|
config.eager_load = false
|
|
10
10
|
|
|
11
11
|
# Configure static asset server for tests with Cache-Control for performance
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
config.serve_static_files = true
|
|
15
|
-
else
|
|
16
|
-
config.serve_static_assets = true
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
config.static_cache_control = "public, max-age=3600"
|
|
12
|
+
config.public_file_server.enabled = true
|
|
13
|
+
config.public_file_server.headers = { "Cache-Control" => "public, max-age=3600" }
|
|
20
14
|
|
|
21
15
|
# Show full error reports and disable caching
|
|
22
16
|
config.consider_all_requests_local = true
|
|
@@ -38,4 +32,5 @@ Dummy::Application.configure do
|
|
|
38
32
|
|
|
39
33
|
# For testing session variables in Capybara specs
|
|
40
34
|
config.middleware.use RackSessionAccess::Middleware
|
|
35
|
+
config.assets.check_precompiled_asset = false
|
|
41
36
|
end
|
|
@@ -3,8 +3,10 @@ Dummy::Application.routes.draw do
|
|
|
3
3
|
root to: "home#index"
|
|
4
4
|
|
|
5
5
|
match "/dashboard", to: "home#dashboard", as: :dashboard, via: [:get]
|
|
6
|
+
get "/secret", to: "home#secret", as: :secret
|
|
6
7
|
|
|
7
8
|
devise_for :users
|
|
9
|
+
devise_for :secure_users
|
|
8
10
|
|
|
9
11
|
# The priority is based upon order of creation:
|
|
10
12
|
# first created -> highest priority.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class AddOtpColumnsToUsers < ActiveRecord::Migration[5.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :users, :otp_secret_key, :string
|
|
4
|
+
add_column :users, :direct_otp, :string
|
|
5
|
+
add_column :users, :direct_otp_sent_at, :datetime
|
|
6
|
+
add_column :users, :totp_timestamp, :timestamp
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# A second 2FA scope whose TOTP secret is encrypted.
|
|
2
|
+
class DeviseCreateSecureUsers < ActiveRecord::Migration[5.0]
|
|
3
|
+
def change
|
|
4
|
+
create_table(:secure_users) do |t|
|
|
5
|
+
t.string :email, null: false, default: ""
|
|
6
|
+
t.string :encrypted_password, null: false, default: ""
|
|
7
|
+
|
|
8
|
+
t.integer :second_factor_attempts_count, default: 0
|
|
9
|
+
t.string :encrypted_otp_secret_key
|
|
10
|
+
t.string :encrypted_otp_secret_key_iv
|
|
11
|
+
t.string :encrypted_otp_secret_key_salt
|
|
12
|
+
t.string :direct_otp
|
|
13
|
+
t.datetime :direct_otp_sent_at
|
|
14
|
+
t.timestamp :totp_timestamp
|
|
15
|
+
|
|
16
|
+
t.timestamps null: false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :secure_users, :email, unique: true
|
|
20
|
+
add_index :secure_users, :encrypted_otp_secret_key, unique: true
|
|
21
|
+
end
|
|
22
|
+
end
|
data/spec/rails_app/db/schema.rb
CHANGED
|
@@ -1,58 +1,73 @@
|
|
|
1
|
-
# encoding: UTF-8
|
|
2
1
|
# This file is auto-generated from the current state of the database. Instead
|
|
3
2
|
# of editing this file, please use the migrations feature of Active Record to
|
|
4
3
|
# incrementally modify your database, and then regenerate this schema definition.
|
|
5
4
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# from scratch.
|
|
10
|
-
#
|
|
5
|
+
# This file is the source Rails uses to define your schema when running `bin/rails
|
|
6
|
+
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
|
7
|
+
# be faster and is potentially less error prone than running all of your
|
|
8
|
+
# migrations from scratch. Old migrations may fail to apply correctly if those
|
|
9
|
+
# migrations use external dependencies or application code.
|
|
11
10
|
#
|
|
12
11
|
# It's strongly recommended that you check this file into your version control system.
|
|
13
12
|
|
|
14
|
-
ActiveRecord::Schema.define(version:
|
|
15
|
-
|
|
13
|
+
ActiveRecord::Schema.define(version: 2026_09_29_000001) do
|
|
16
14
|
create_table "admins", force: :cascade do |t|
|
|
17
|
-
t.string
|
|
18
|
-
t.string
|
|
19
|
-
t.string
|
|
15
|
+
t.string "email", default: "", null: false
|
|
16
|
+
t.string "encrypted_password", default: "", null: false
|
|
17
|
+
t.string "reset_password_token"
|
|
20
18
|
t.datetime "reset_password_sent_at"
|
|
21
19
|
t.datetime "remember_created_at"
|
|
22
|
-
t.integer
|
|
20
|
+
t.integer "sign_in_count", default: 0, null: false
|
|
23
21
|
t.datetime "current_sign_in_at"
|
|
24
22
|
t.datetime "last_sign_in_at"
|
|
25
|
-
t.string
|
|
26
|
-
t.string
|
|
27
|
-
t.datetime "created_at",
|
|
28
|
-
t.datetime "updated_at",
|
|
23
|
+
t.string "current_sign_in_ip"
|
|
24
|
+
t.string "last_sign_in_ip"
|
|
25
|
+
t.datetime "created_at", null: false
|
|
26
|
+
t.datetime "updated_at", null: false
|
|
27
|
+
t.index ["email"], name: "index_admins_on_email", unique: true
|
|
28
|
+
t.index ["reset_password_token"], name: "index_admins_on_reset_password_token", unique: true
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
create_table "secure_users", force: :cascade do |t|
|
|
32
|
+
t.string "email", default: "", null: false
|
|
33
|
+
t.string "encrypted_password", default: "", null: false
|
|
34
|
+
t.integer "second_factor_attempts_count", default: 0
|
|
35
|
+
t.string "encrypted_otp_secret_key"
|
|
36
|
+
t.string "encrypted_otp_secret_key_iv"
|
|
37
|
+
t.string "encrypted_otp_secret_key_salt"
|
|
38
|
+
t.string "direct_otp"
|
|
39
|
+
t.datetime "direct_otp_sent_at"
|
|
40
|
+
t.datetime "totp_timestamp"
|
|
41
|
+
t.datetime "created_at", null: false
|
|
42
|
+
t.datetime "updated_at", null: false
|
|
43
|
+
t.index ["email"], name: "index_secure_users_on_email", unique: true
|
|
44
|
+
t.index ["encrypted_otp_secret_key"], name: "index_secure_users_on_encrypted_otp_secret_key", unique: true
|
|
45
|
+
end
|
|
33
46
|
|
|
34
47
|
create_table "users", force: :cascade do |t|
|
|
35
|
-
t.string
|
|
36
|
-
t.string
|
|
37
|
-
t.string
|
|
48
|
+
t.string "email", default: "", null: false
|
|
49
|
+
t.string "encrypted_password", default: "", null: false
|
|
50
|
+
t.string "reset_password_token"
|
|
38
51
|
t.datetime "reset_password_sent_at"
|
|
39
52
|
t.datetime "remember_created_at"
|
|
40
|
-
t.integer
|
|
53
|
+
t.integer "sign_in_count", default: 0, null: false
|
|
41
54
|
t.datetime "current_sign_in_at"
|
|
42
55
|
t.datetime "last_sign_in_at"
|
|
43
|
-
t.string
|
|
44
|
-
t.string
|
|
45
|
-
t.datetime "created_at",
|
|
46
|
-
t.datetime "updated_at",
|
|
47
|
-
t.integer
|
|
48
|
-
t.string
|
|
49
|
-
t.string
|
|
50
|
-
t.string
|
|
51
|
-
t.string
|
|
56
|
+
t.string "current_sign_in_ip"
|
|
57
|
+
t.string "last_sign_in_ip"
|
|
58
|
+
t.datetime "created_at", null: false
|
|
59
|
+
t.datetime "updated_at", null: false
|
|
60
|
+
t.integer "second_factor_attempts_count", default: 0
|
|
61
|
+
t.string "nickname", limit: 64
|
|
62
|
+
t.string "encrypted_otp_secret_key"
|
|
63
|
+
t.string "encrypted_otp_secret_key_iv"
|
|
64
|
+
t.string "encrypted_otp_secret_key_salt"
|
|
65
|
+
t.string "otp_secret_key"
|
|
66
|
+
t.string "direct_otp"
|
|
67
|
+
t.datetime "direct_otp_sent_at"
|
|
68
|
+
t.datetime "totp_timestamp"
|
|
69
|
+
t.index ["email"], name: "index_users_on_email", unique: true
|
|
70
|
+
t.index ["encrypted_otp_secret_key"], name: "index_users_on_encrypted_otp_secret_key", unique: true
|
|
71
|
+
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
|
52
72
|
end
|
|
53
|
-
|
|
54
|
-
add_index "users", ["email"], name: "index_users_on_email", unique: true
|
|
55
|
-
add_index "users", ["encrypted_otp_secret_key"], name: "index_users_on_encrypted_otp_secret_key", unique: true
|
|
56
|
-
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
|
57
|
-
|
|
58
73
|
end
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
require 'ostruct'
|
|
2
|
-
|
|
3
1
|
class SMSProvider
|
|
4
|
-
Message =
|
|
2
|
+
Message = Struct.new(:to, :body)
|
|
5
3
|
|
|
6
4
|
class_attribute :messages
|
|
7
5
|
self.messages = []
|
|
8
6
|
|
|
9
7
|
def self.send_message(opts = {})
|
|
10
|
-
self.messages << Message.new(opts)
|
|
8
|
+
self.messages << Message.new(opts[:to], opts[:body])
|
|
11
9
|
end
|
|
12
10
|
|
|
13
11
|
def self.last_message
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'Requests from a user who has not completed two factor authentication', type: :request do
|
|
4
|
+
let(:user) { create_user }
|
|
5
|
+
|
|
6
|
+
before { login_as user, scope: :user }
|
|
7
|
+
|
|
8
|
+
it 'redirects HTML requests to the two factor page' do
|
|
9
|
+
get secret_path
|
|
10
|
+
|
|
11
|
+
expect(response).to redirect_to(user_two_factor_authentication_path)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'responds to JSON requests with 401 and the two factor path' do
|
|
15
|
+
get secret_path(format: :json)
|
|
16
|
+
|
|
17
|
+
expect(response).to have_http_status(:unauthorized)
|
|
18
|
+
expect(response.parsed_body).to eq('redirect_to' => user_two_factor_authentication_path)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'responds to other formats with 401 without running the action' do
|
|
22
|
+
get secret_path(format: :xml)
|
|
23
|
+
|
|
24
|
+
expect(response).to have_http_status(:unauthorized)
|
|
25
|
+
expect(response.body).not_to include('top secret')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'responds with 401 when the format comes from the Accept header' do
|
|
29
|
+
get secret_path, headers: { 'Accept' => 'text/csv' }
|
|
30
|
+
|
|
31
|
+
expect(response).to have_http_status(:unauthorized)
|
|
32
|
+
expect(response.body).not_to include('top secret')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context 'when the user does not need two factor authentication' do
|
|
36
|
+
before do
|
|
37
|
+
allow_any_instance_of(User).to receive(:need_two_factor_authentication?).and_return(false)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'runs the action and sends no code' do
|
|
41
|
+
get secret_path(format: :xml)
|
|
42
|
+
|
|
43
|
+
expect(response).to have_http_status(:ok)
|
|
44
|
+
expect(response.body).to eq('top secret')
|
|
45
|
+
expect(SMSProvider.messages).to be_empty
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -3,9 +3,7 @@ module AuthenticatedModelHelper
|
|
|
3
3
|
GuestUser.new
|
|
4
4
|
end
|
|
5
5
|
|
|
6
|
-
def create_user(
|
|
7
|
-
create_table_for_nonencrypted_user if type == 'not_encrypted'
|
|
8
|
-
|
|
6
|
+
def create_user(attributes = {})
|
|
9
7
|
User.create!(valid_attributes(attributes))
|
|
10
8
|
end
|
|
11
9
|
|
|
@@ -13,6 +11,10 @@ module AuthenticatedModelHelper
|
|
|
13
11
|
Admin.create!(valid_attributes.except(:nickname))
|
|
14
12
|
end
|
|
15
13
|
|
|
14
|
+
def create_secure_user(attributes = {})
|
|
15
|
+
SecureUser.create!(valid_attributes(attributes).except(:nickname))
|
|
16
|
+
end
|
|
17
|
+
|
|
16
18
|
def valid_attributes(attributes={})
|
|
17
19
|
{
|
|
18
20
|
nickname: 'Marissa',
|
|
@@ -27,33 +29,6 @@ module AuthenticatedModelHelper
|
|
|
27
29
|
@@email_count += 1
|
|
28
30
|
"user#{@@email_count}@example.com"
|
|
29
31
|
end
|
|
30
|
-
|
|
31
|
-
def create_table_for_nonencrypted_user
|
|
32
|
-
silence_stream(STDOUT) do
|
|
33
|
-
ActiveRecord::Schema.define(version: 1) do
|
|
34
|
-
create_table 'users', force: :cascade do |t|
|
|
35
|
-
t.string 'email', default: '', null: false
|
|
36
|
-
t.string 'encrypted_password', default: '', null: false
|
|
37
|
-
t.string 'reset_password_token'
|
|
38
|
-
t.datetime 'reset_password_sent_at'
|
|
39
|
-
t.datetime 'remember_created_at'
|
|
40
|
-
t.integer 'sign_in_count', default: 0, null: false
|
|
41
|
-
t.datetime 'current_sign_in_at'
|
|
42
|
-
t.datetime 'last_sign_in_at'
|
|
43
|
-
t.string 'current_sign_in_ip'
|
|
44
|
-
t.string 'last_sign_in_ip'
|
|
45
|
-
t.datetime 'created_at', null: false
|
|
46
|
-
t.datetime 'updated_at', null: false
|
|
47
|
-
t.integer 'second_factor_attempts_count', default: 0
|
|
48
|
-
t.string 'nickname', limit: 64
|
|
49
|
-
t.string 'otp_secret_key'
|
|
50
|
-
t.string 'direct_otp'
|
|
51
|
-
t.datetime 'direct_otp_sent_at'
|
|
52
|
-
t.timestamp 'totp_timestamp'
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
32
|
end
|
|
58
33
|
|
|
59
34
|
RSpec.configuration.send(:include, AuthenticatedModelHelper)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
module ControllerHelper
|
|
2
|
-
def sign_in(user = create_user
|
|
3
|
-
allow(warden).to receive(:authenticated?).
|
|
4
|
-
allow(
|
|
5
|
-
|
|
2
|
+
def sign_in(user = create_user, scope: :user)
|
|
3
|
+
allow(warden).to receive(:authenticated?).and_call_original
|
|
4
|
+
allow(warden).to receive(:authenticated?).with(scope).and_return(true)
|
|
5
|
+
allow(controller).to receive(:"current_#{scope}").and_return(user)
|
|
6
|
+
warden.session(scope)[TwoFactorAuthentication::NEED_AUTHENTICATION] = true
|
|
6
7
|
end
|
|
7
8
|
end
|
|
8
9
|
|