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.
Files changed (55) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +77 -0
  3. data/.gitignore +3 -0
  4. data/CHANGELOG.md +37 -2
  5. data/Gemfile +29 -9
  6. data/MIGRATION_GUIDE.md +143 -0
  7. data/README.md +217 -28
  8. data/app/controllers/devise/two_factor_authentication_controller.rb +3 -2
  9. data/app/views/devise/two_factor_authentication/show.html.erb +4 -4
  10. data/config/locales/de.yml +8 -0
  11. data/lib/generators/active_record/templates/migration.rb +9 -1
  12. data/lib/generators/active_record/two_factor_authentication_generator.rb +18 -0
  13. data/lib/generators/two_factor_authentication/two_factor_authentication_generator.rb +1 -1
  14. data/lib/two_factor_authentication/controllers/helpers.rb +8 -4
  15. data/lib/two_factor_authentication/models/two_factor_authenticatable.rb +16 -6
  16. data/lib/two_factor_authentication/orm/active_record.rb +2 -0
  17. data/lib/two_factor_authentication/routes.rb +1 -1
  18. data/lib/two_factor_authentication/schema.rb +7 -7
  19. data/lib/two_factor_authentication/version.rb +1 -1
  20. data/lib/two_factor_authentication.rb +1 -2
  21. data/spec/controllers/two_factor_authentication_controller_spec.rb +87 -2
  22. data/spec/features/two_factor_authenticatable_spec.rb +119 -14
  23. data/spec/generators/active_record/two_factor_authentication_generator_spec.rb +52 -4
  24. data/spec/generators/two_factor_authentication/two_factor_authentication_generator_spec.rb +37 -0
  25. data/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb +97 -10
  26. data/spec/lib/two_factor_authentication/schema_spec.rb +41 -0
  27. data/spec/rails_app/app/assets/config/manifest.js +2 -0
  28. data/spec/rails_app/app/controllers/home_controller.rb +6 -1
  29. data/spec/rails_app/app/models/guest_user.rb +1 -1
  30. data/spec/rails_app/app/models/secure_user.rb +13 -0
  31. data/spec/rails_app/config/application.rb +1 -1
  32. data/spec/rails_app/config/environments/test.rb +3 -8
  33. data/spec/rails_app/config/initializers/inflections.rb +4 -0
  34. data/spec/rails_app/config/routes.rb +2 -0
  35. data/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb +1 -1
  36. data/spec/rails_app/db/migrate/20140407172619_two_factor_authentication_add_to_users.rb +1 -1
  37. data/spec/rails_app/db/migrate/20140407215513_add_nickanme_to_users.rb +1 -1
  38. data/spec/rails_app/db/migrate/20151224171231_add_encrypted_columns_to_user.rb +1 -1
  39. data/spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb +1 -1
  40. data/spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb +1 -1
  41. data/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb +1 -1
  42. data/spec/rails_app/db/migrate/20260929000000_add_otp_columns_to_users.rb +8 -0
  43. data/spec/rails_app/db/migrate/20260929000001_devise_create_secure_users.rb +22 -0
  44. data/spec/rails_app/db/schema.rb +51 -36
  45. data/spec/rails_app/lib/sms_provider.rb +2 -4
  46. data/spec/requests/two_factor_authentication_helpers_spec.rb +48 -0
  47. data/spec/spec_helper.rb +1 -0
  48. data/spec/support/authenticated_model_helper.rb +5 -30
  49. data/spec/support/controller_helper.rb +5 -4
  50. data/spec/support/features_spec_helper.rb +1 -0
  51. data/spec/support/totp_helper.rb +1 -1
  52. data/two_factor_authentication.gemspec +5 -6
  53. metadata +45 -28
  54. data/.travis.yml +0 -29
  55. 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
- expect(instance.provisioning_uri).
136
- to match(%r{otpauth://totp/houdini@example.com\?secret=\w{16}})
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{16}$})
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 = CGI.parse(uri.query)
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'].shift).to eq('Magic')
153
- expect(params['secret'].shift).to match(/\w{16}/)
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 16 character string' do
175
+ it 'returns a 32 character string' do
167
176
  secret = instance.generate_totp_secret
168
177
 
169
- expect(secret).to match(/\w{16}/)
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
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts .js
2
+ //= link_directory ../stylesheets .css
@@ -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
@@ -7,7 +7,7 @@ class GuestUser
7
7
  attr_accessor :direct_otp, :direct_otp_sent_at, :otp_secret_key, :email,
8
8
  :second_factor_attempts_count, :totp_timestamp
9
9
 
10
- def update_attributes(attrs)
10
+ def update(attrs)
11
11
  attrs.each do |key, value|
12
12
  send(key.to_s + '=', value)
13
13
  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
@@ -1,3 +1,4 @@
1
+ require 'logger'
1
2
  require File.expand_path('../boot', __FILE__)
2
3
 
3
4
  require "active_record/railtie"
@@ -60,4 +61,3 @@ module Dummy
60
61
  config.secret_key_base = 'secretvalue'
61
62
  end
62
63
  end
63
-
@@ -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
- if Rails::VERSION::MAJOR == 4 && Rails::VERSION::MINOR >= 2 ||
13
- Rails::VERSION::MAJOR >= 5
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
@@ -13,3 +13,7 @@
13
13
  # ActiveSupport::Inflector.inflections do |inflect|
14
14
  # inflect.acronym 'RESTful'
15
15
  # end
16
+
17
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
18
+ inflect.acronym 'SMS'
19
+ 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.
@@ -1,4 +1,4 @@
1
- class DeviseCreateUsers < ActiveRecord::Migration
1
+ class DeviseCreateUsers < ActiveRecord::Migration[4.2]
2
2
  def change
3
3
  create_table(:users) do |t|
4
4
  ## Database authenticatable
@@ -1,4 +1,4 @@
1
- class TwoFactorAuthenticationAddToUsers < ActiveRecord::Migration
1
+ class TwoFactorAuthenticationAddToUsers < ActiveRecord::Migration[4.2]
2
2
  def up
3
3
  change_table :users do |t|
4
4
  t.string :otp_secret_key
@@ -1,4 +1,4 @@
1
- class AddNickanmeToUsers < ActiveRecord::Migration
1
+ class AddNickanmeToUsers < ActiveRecord::Migration[4.2]
2
2
  def change
3
3
  change_table :users do |t|
4
4
  t.column :nickname, :string, limit: 64
@@ -1,4 +1,4 @@
1
- class AddEncryptedColumnsToUser < ActiveRecord::Migration
1
+ class AddEncryptedColumnsToUser < ActiveRecord::Migration[4.2]
2
2
  def change
3
3
  add_column :users, :encrypted_otp_secret_key, :string
4
4
  add_column :users, :encrypted_otp_secret_key_iv, :string
@@ -1,4 +1,4 @@
1
- class PopulateOtpColumn < ActiveRecord::Migration
1
+ class PopulateOtpColumn < ActiveRecord::Migration[4.2]
2
2
  def up
3
3
  User.reset_column_information
4
4
 
@@ -1,4 +1,4 @@
1
- class RemoveOtpSecretKeyFromUser < ActiveRecord::Migration
1
+ class RemoveOtpSecretKeyFromUser < ActiveRecord::Migration[4.2]
2
2
  def change
3
3
  remove_column :users, :otp_secret_key, :string
4
4
  end
@@ -1,4 +1,4 @@
1
- class DeviseCreateAdmins < ActiveRecord::Migration
1
+ class DeviseCreateAdmins < ActiveRecord::Migration[4.2]
2
2
  def change
3
3
  create_table(:admins) do |t|
4
4
  ## Database authenticatable
@@ -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
@@ -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
- # Note that this schema.rb definition is the authoritative source for your
7
- # database schema. If you need to create the application database on another
8
- # system, you should be using db:schema:load, not running all the migrations
9
- # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10
- # you'll amass, the slower it'll run and the greater likelihood for issues).
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: 20160209032439) do
15
-
13
+ ActiveRecord::Schema.define(version: 2026_09_29_000001) do
16
14
  create_table "admins", force: :cascade do |t|
17
- t.string "email", default: "", null: false
18
- t.string "encrypted_password", default: "", null: false
19
- t.string "reset_password_token"
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 "sign_in_count", default: 0, null: false
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 "current_sign_in_ip"
26
- t.string "last_sign_in_ip"
27
- t.datetime "created_at", null: false
28
- t.datetime "updated_at", null: false
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
- add_index "admins", ["email"], name: "index_admins_on_email", unique: true
32
- add_index "admins", ["reset_password_token"], name: "index_admins_on_reset_password_token", unique: true
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 "email", default: "", null: false
36
- t.string "encrypted_password", default: "", null: false
37
- t.string "reset_password_token"
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 "sign_in_count", default: 0, null: false
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 "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 "encrypted_otp_secret_key"
50
- t.string "encrypted_otp_secret_key_iv"
51
- t.string "encrypted_otp_secret_key_salt"
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 = Class.new(OpenStruct)
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
@@ -1,5 +1,6 @@
1
1
  ENV["RAILS_ENV"] ||= "test"
2
2
  require File.expand_path("../rails_app/config/environment.rb", __FILE__)
3
+ require File.expand_path("../rails_app/lib/sms_provider.rb", __FILE__)
3
4
 
4
5
  require 'rspec/rails'
5
6
  require 'timecop'
@@ -3,9 +3,7 @@ module AuthenticatedModelHelper
3
3
  GuestUser.new
4
4
  end
5
5
 
6
- def create_user(type = 'encrypted', attributes = {})
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('not_encrypted'))
3
- allow(warden).to receive(:authenticated?).with(:user).and_return(true)
4
- allow(controller).to receive(:current_user).and_return(user)
5
- warden.session(:user)[TwoFactorAuthentication::NEED_AUTHENTICATION] = true
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
 
@@ -30,6 +30,7 @@ end
30
30
 
31
31
  RSpec.configure do |config|
32
32
  config.include Warden::Test::Helpers, type: :feature
33
+ config.include Warden::Test::Helpers, type: :request
33
34
  config.include FeaturesSpecHelper, type: :feature
34
35
 
35
36
  config.before(:each) do
@@ -6,6 +6,6 @@ class TotpHelper
6
6
  end
7
7
 
8
8
  def totp_code(time = Time.now)
9
- ROTP::TOTP.new(@secret_key, digits: @otp_length).at(time, true)
9
+ ROTP::TOTP.new(@secret_key, digits: @otp_length).at(time)
10
10
  end
11
11
  end