devise-otp 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +0 -2
  3. data/.gitignore +3 -1
  4. data/CHANGELOG.md +51 -8
  5. data/README.md +8 -2
  6. data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +46 -27
  7. data/app/controllers/devise_otp/devise/otp_tokens_controller.rb +24 -6
  8. data/app/views/devise/otp_credentials/show.html.erb +6 -6
  9. data/app/views/devise/otp_tokens/_token_secret.html.erb +3 -4
  10. data/app/views/devise/otp_tokens/edit.html.erb +26 -0
  11. data/app/views/devise/otp_tokens/show.html.erb +7 -14
  12. data/config/locales/en.yml +23 -14
  13. data/devise-otp.gemspec +1 -2
  14. data/lib/devise/strategies/database_authenticatable.rb +64 -0
  15. data/lib/devise-otp/version.rb +1 -1
  16. data/lib/devise-otp.rb +31 -11
  17. data/lib/devise_otp_authenticatable/controllers/helpers.rb +9 -10
  18. data/lib/devise_otp_authenticatable/controllers/public_helpers.rb +39 -0
  19. data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +10 -0
  20. data/lib/devise_otp_authenticatable/engine.rb +2 -5
  21. data/lib/devise_otp_authenticatable/hooks/refreshable.rb +5 -0
  22. data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +22 -20
  23. data/lib/devise_otp_authenticatable/routes.rb +3 -1
  24. data/test/dummy/app/controllers/admin_posts_controller.rb +85 -0
  25. data/test/dummy/app/controllers/application_controller.rb +0 -1
  26. data/test/dummy/app/controllers/base_controller.rb +6 -0
  27. data/test/dummy/app/models/admin.rb +25 -0
  28. data/test/dummy/app/views/admin_posts/_form.html.erb +25 -0
  29. data/test/dummy/app/views/admin_posts/edit.html.erb +6 -0
  30. data/test/dummy/app/views/admin_posts/index.html.erb +25 -0
  31. data/test/dummy/app/views/admin_posts/new.html.erb +5 -0
  32. data/test/dummy/app/views/admin_posts/show.html.erb +15 -0
  33. data/test/dummy/app/views/base/home.html.erb +1 -0
  34. data/test/dummy/config/application.rb +0 -2
  35. data/test/dummy/config/routes.rb +4 -1
  36. data/test/dummy/db/migrate/20240604000001_create_admins.rb +9 -0
  37. data/test/dummy/db/migrate/20240604000002_add_devise_to_admins.rb +52 -0
  38. data/test/dummy/db/migrate/20240604000003_devise_otp_add_to_admins.rb +28 -0
  39. data/test/integration/disable_token_test.rb +53 -0
  40. data/test/integration/enable_otp_form_test.rb +57 -0
  41. data/test/integration/persistence_test.rb +3 -6
  42. data/test/integration/refresh_test.rb +32 -0
  43. data/test/integration/reset_token_test.rb +45 -0
  44. data/test/integration/sign_in_test.rb +10 -14
  45. data/test/integration/trackable_test.rb +50 -0
  46. data/test/integration_tests_helper.rb +24 -6
  47. data/test/models/otp_authenticatable_test.rb +62 -27
  48. data/test/test_helper.rb +1 -71
  49. metadata +26 -23
  50. data/lib/devise_otp_authenticatable/hooks/sessions.rb +0 -58
  51. data/lib/devise_otp_authenticatable/hooks.rb +0 -11
  52. data/test/integration/token_test.rb +0 -30
@@ -39,7 +39,6 @@ module DeviseOtpAuthenticatable
39
39
  end
40
40
  end
41
41
 
42
- # fixme do cookies and persistence need to be scoped? probably
43
42
  #
44
43
  # check if the resource needs a credentials refresh. IE, they need to be asked a password again to access
45
44
  # this resource.
@@ -47,8 +46,8 @@ module DeviseOtpAuthenticatable
47
46
  def needs_credentials_refresh?(resource)
48
47
  return false unless resource.class.otp_credentials_refresh
49
48
 
50
- (!session[otp_scoped_refresh_property].present? ||
51
- (session[otp_scoped_refresh_property] < DateTime.now)).tap { |need| otp_set_refresh_return_url if need }
49
+ (!warden.session(resource_name)[otp_refresh_property].present? ||
50
+ (warden.session(resource_name)[otp_refresh_property] < DateTime.now)).tap { |need| otp_set_refresh_return_url if need }
52
51
  end
53
52
 
54
53
  #
@@ -56,7 +55,7 @@ module DeviseOtpAuthenticatable
56
55
  #
57
56
  def otp_refresh_credentials_for(resource)
58
57
  return false unless resource.class.otp_credentials_refresh
59
- session[otp_scoped_refresh_property] = (Time.now + resource.class.otp_credentials_refresh)
58
+ warden.session(resource_name)[otp_refresh_property] = (Time.now + resource.class.otp_credentials_refresh)
60
59
  end
61
60
 
62
61
  #
@@ -85,19 +84,19 @@ module DeviseOtpAuthenticatable
85
84
  end
86
85
 
87
86
  def otp_set_refresh_return_url
88
- session[otp_scoped_refresh_return_url_property] = request.fullpath
87
+ warden.session(resource_name)[otp_refresh_return_url_property] = request.fullpath
89
88
  end
90
89
 
91
90
  def otp_fetch_refresh_return_url
92
- session.delete(otp_scoped_refresh_return_url_property) { :root }
91
+ warden.session(resource_name).delete(otp_refresh_return_url_property) { :root }
93
92
  end
94
93
 
95
- def otp_scoped_refresh_return_url_property
96
- "otp_#{resource_name}refresh_return_url".to_sym
94
+ def otp_refresh_return_url_property
95
+ "refresh_return_url"
97
96
  end
98
97
 
99
- def otp_scoped_refresh_property
100
- "otp_#{resource_name}refresh_after".to_sym
98
+ def otp_refresh_property
99
+ "credentials_refreshed_at"
101
100
  end
102
101
 
103
102
  def otp_scoped_persistence_cookie
@@ -0,0 +1,39 @@
1
+ module DeviseOtpAuthenticatable
2
+ module Controllers
3
+ module PublicHelpers
4
+ extend ActiveSupport::Concern
5
+
6
+ def self.generate_helpers!
7
+ Devise.mappings.each do |key, mapping|
8
+ self.define_helpers(mapping)
9
+ end
10
+ end
11
+
12
+ def self.define_helpers(mapping) #:nodoc:
13
+ mapping = mapping.name
14
+
15
+ class_eval <<-METHODS, __FILE__, __LINE__ + 1
16
+ def ensure_mandatory_#{mapping}_otp!
17
+ resource = current_#{mapping}
18
+ if !devise_controller?
19
+ if mandatory_otp_missing_on?(resource)
20
+ redirect_to edit_#{mapping}_otp_token_path
21
+ end
22
+ end
23
+ end
24
+ METHODS
25
+ end
26
+
27
+ def otp_mandatory_on?(resource)
28
+ return false unless resource.respond_to?(:otp_mandatory)
29
+
30
+ resource.class.otp_mandatory or resource.otp_mandatory
31
+ end
32
+
33
+ def mandatory_otp_missing_on?(resource)
34
+ otp_mandatory_on?(resource) && !resource.otp_enabled
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -21,6 +21,16 @@ module DeviseOtpAuthenticatable
21
21
  send("#{scope}_otp_token_path", opts)
22
22
  end
23
23
 
24
+ def edit_otp_token_path_for(resource_or_scope, opts = {})
25
+ scope = ::Devise::Mapping.find_scope!(resource_or_scope)
26
+ send("edit_#{scope}_otp_token_path", opts)
27
+ end
28
+
29
+ def reset_otp_token_path_for(resource_or_scope, opts = {})
30
+ scope = ::Devise::Mapping.find_scope!(resource_or_scope)
31
+ send("reset_#{scope}_otp_token_path", opts)
32
+ end
33
+
24
34
  def otp_credential_path_for(resource_or_scope, opts = {})
25
35
  scope = ::Devise::Mapping.find_scope!(resource_or_scope)
26
36
  send("#{scope}_otp_credential_path", opts)
@@ -3,20 +3,17 @@ module DeviseOtpAuthenticatable
3
3
  config.devise_otp = ActiveSupport::OrderedOptions.new
4
4
  config.devise_otp.precompile_assets = true
5
5
 
6
- # We use to_prepare instead of after_initialize here because Devise is a Rails engine;
7
- config.to_prepare do
8
- DeviseOtpAuthenticatable::Hooks.apply
9
- end
10
-
11
6
  initializer "devise-otp", group: :all do |app|
12
7
  ActiveSupport.on_load(:devise_controller) do
13
8
  include DeviseOtpAuthenticatable::Controllers::UrlHelpers
14
9
  include DeviseOtpAuthenticatable::Controllers::Helpers
10
+ include DeviseOtpAuthenticatable::Controllers::PublicHelpers
15
11
  end
16
12
 
17
13
  ActiveSupport.on_load(:action_view) do
18
14
  include DeviseOtpAuthenticatable::Controllers::UrlHelpers
19
15
  include DeviseOtpAuthenticatable::Controllers::Helpers
16
+ include DeviseOtpAuthenticatable::Controllers::PublicHelpers
20
17
  end
21
18
 
22
19
  # See: https://guides.rubyonrails.org/engines.html#separate-assets-and-precompiling
@@ -0,0 +1,5 @@
1
+ # After each sign in, update credentials refreshed at time
2
+ Warden::Manager.after_set_user except: :fetch do |record, warden, options|
3
+ warden.session(options[:scope])["credentials_refreshed_at"] = (Time.now + record.class.otp_credentials_refresh)
4
+ end
5
+
@@ -5,8 +5,6 @@ module Devise::Models
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- before_validation :generate_otp_auth_secret, on: :create
9
- before_validation :generate_otp_persistence_seed, on: :create
10
8
  scope :with_valid_otp_challenge, lambda { |time| where("otp_challenge_expires > ?", time) }
11
9
  end
12
10
 
@@ -36,21 +34,6 @@ module Devise::Models
36
34
  email
37
35
  end
38
36
 
39
- def reset_otp_credentials
40
- @time_based_otp = nil
41
- @recovery_otp = nil
42
- generate_otp_auth_secret
43
- reset_otp_persistence
44
- update!(otp_enabled: false,
45
- otp_session_challenge: nil, otp_challenge_expires: nil,
46
- otp_recovery_counter: 0)
47
- end
48
-
49
- def reset_otp_credentials!
50
- reset_otp_credentials
51
- save!
52
- end
53
-
54
37
  def reset_otp_persistence
55
38
  generate_otp_persistence_seed
56
39
  end
@@ -60,11 +43,30 @@ module Devise::Models
60
43
  save!
61
44
  end
62
45
 
63
- def enable_otp!
64
- if otp_persistence_seed.nil?
65
- reset_otp_credentials!
46
+ def populate_otp_secrets!
47
+ if [otp_auth_secret, otp_recovery_secret, otp_persistence_seed].any? { |a| a.blank? }
48
+ generate_otp_auth_secret
49
+ generate_otp_persistence_seed
50
+ self.save!
66
51
  end
52
+ end
53
+
54
+ def clear_otp_fields!
55
+ @time_based_otp = nil
56
+ @recovery_otp = nil
57
+
58
+ self.update!(
59
+ :otp_auth_secret => nil,
60
+ :otp_recovery_secret => nil,
61
+ :otp_persistence_seed => nil,
62
+ :otp_session_challenge => nil,
63
+ :otp_challenge_expires => nil,
64
+ :otp_failed_attempts => 0,
65
+ :otp_recovery_counter => 0
66
+ )
67
+ end
67
68
 
69
+ def enable_otp!
68
70
  update!(otp_enabled: true, otp_enabled_on: Time.now)
69
71
  end
70
72
 
@@ -4,7 +4,7 @@ module ActionDispatch::Routing
4
4
 
5
5
  def devise_otp(mapping, controllers)
6
6
  namespace :otp, module: :devise_otp do
7
- resource :token, only: [:show, :update, :destroy],
7
+ resource :token, only: [:show, :edit, :update, :destroy],
8
8
  path: mapping.path_names[:token], controller: controllers[:otp_tokens] do
9
9
  if Devise.otp_trust_persistence
10
10
  get :persistence, action: "get_persistence"
@@ -13,6 +13,7 @@ module ActionDispatch::Routing
13
13
  end
14
14
 
15
15
  get :recovery
16
+ post :reset
16
17
  end
17
18
 
18
19
  resource :credential, only: [:show, :update],
@@ -20,6 +21,7 @@ module ActionDispatch::Routing
20
21
  get :refresh, action: "get_refresh"
21
22
  put :refresh, action: "set_refresh"
22
23
  end
24
+
23
25
  end
24
26
  end
25
27
  end
@@ -0,0 +1,85 @@
1
+ class AdminPostsController < ApplicationController
2
+ before_action :authenticate_admin!
3
+
4
+ # GET /posts
5
+ # GET /posts.json
6
+ def index
7
+ @posts = Post.all
8
+
9
+ respond_to do |format|
10
+ format.html # index.html.erb
11
+ format.json { render json: @posts }
12
+ end
13
+ end
14
+
15
+ # GET /posts/1
16
+ # GET /posts/1.json
17
+ def show
18
+ @post = Post.find(params[:id])
19
+
20
+ respond_to do |format|
21
+ format.html # show.html.erb
22
+ format.json { render json: @post }
23
+ end
24
+ end
25
+
26
+ # GET /posts/new
27
+ # GET /posts/new.json
28
+ def new
29
+ @post = Post.new
30
+
31
+ respond_to do |format|
32
+ format.html # new.html.erb
33
+ format.json { render json: @post }
34
+ end
35
+ end
36
+
37
+ # GET /posts/1/edit
38
+ def edit
39
+ @post = Post.find(params[:id])
40
+ end
41
+
42
+ # POST /posts
43
+ # POST /posts.json
44
+ def create
45
+ @post = Post.new(params[:post])
46
+
47
+ respond_to do |format|
48
+ if @post.save
49
+ format.html { redirect_to @post, notice: "Post was successfully created." }
50
+ format.json { render json: @post, status: :created, location: @post }
51
+ else
52
+ format.html { render action: "new" }
53
+ format.json { render json: @post.errors, status: :unprocessable_entity }
54
+ end
55
+ end
56
+ end
57
+
58
+ # PUT /posts/1
59
+ # PUT /posts/1.json
60
+ def update
61
+ @post = Post.find(params[:id])
62
+
63
+ respond_to do |format|
64
+ if @post.update_attributes(params[:post])
65
+ format.html { redirect_to @post, notice: "Post was successfully updated." }
66
+ format.json { head :ok }
67
+ else
68
+ format.html { render action: "edit" }
69
+ format.json { render json: @post.errors, status: :unprocessable_entity }
70
+ end
71
+ end
72
+ end
73
+
74
+ # DELETE /posts/1
75
+ # DELETE /posts/1.json
76
+ def destroy
77
+ @post = Post.find(params[:id])
78
+ @post.destroy
79
+
80
+ respond_to do |format|
81
+ format.html { redirect_to posts_url }
82
+ format.json { head :ok }
83
+ end
84
+ end
85
+ end
@@ -1,4 +1,3 @@
1
1
  class ApplicationController < ActionController::Base
2
2
  protect_from_forgery
3
- before_action :authenticate_user!
4
3
  end
@@ -0,0 +1,6 @@
1
+ class BaseController < ApplicationController
2
+
3
+ def home
4
+ end
5
+
6
+ end
@@ -0,0 +1,25 @@
1
+ class Admin < PARENT_MODEL_CLASS
2
+ if DEVISE_ORM == :mongoid
3
+ include Mongoid::Document
4
+
5
+ ## Database authenticatable
6
+ field :email, type: String, null: false, default: ""
7
+ field :encrypted_password, type: String, null: false, default: ""
8
+
9
+ ## Recoverable
10
+ field :reset_password_token, type: String
11
+ field :reset_password_sent_at, type: Time
12
+ end
13
+
14
+ devise :otp_authenticatable, :database_authenticatable, :registerable,
15
+ :trackable, :validatable
16
+
17
+ # Setup accessible (or protected) attributes for your model
18
+ # attr_accessible :otp_enabled, :otp_mandatory, :as => :otp_privileged
19
+ # attr_accessible :email, :password, :password_confirmation, :remember_me
20
+
21
+ def self.otp_mandatory
22
+ true
23
+ end
24
+
25
+ end
@@ -0,0 +1,25 @@
1
+ <%= form_for([:admin, @post]) do |f| %>
2
+ <% if @post.errors.any? %>
3
+ <div id="error_explanation">
4
+ <h2><%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:</h2>
5
+
6
+ <ul>
7
+ <% @post.errors.full_messages.each do |msg| %>
8
+ <li><%= msg %></li>
9
+ <% end %>
10
+ </ul>
11
+ </div>
12
+ <% end %>
13
+
14
+ <div class="field">
15
+ <%= f.label :title %><br />
16
+ <%= f.text_field :title %>
17
+ </div>
18
+ <div class="field">
19
+ <%= f.label :body %><br />
20
+ <%= f.text_area :body %>
21
+ </div>
22
+ <div class="actions">
23
+ <%= f.submit %>
24
+ </div>
25
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <h1>Editing post</h1>
2
+
3
+ <%= render 'form' %>
4
+
5
+ <%= link_to 'Show', @post %> |
6
+ <%= link_to 'Back', admin_posts_path %>
@@ -0,0 +1,25 @@
1
+ <h1>Listing posts</h1>
2
+
3
+ <table>
4
+ <tr>
5
+ <th>Title</th>
6
+ <th>Body</th>
7
+ <th></th>
8
+ <th></th>
9
+ <th></th>
10
+ </tr>
11
+
12
+ <% @posts.each do |post| %>
13
+ <tr>
14
+ <td><%= post.title %></td>
15
+ <td><%= post.body %></td>
16
+ <td><%= link_to 'Show', post %></td>
17
+ <td><%= link_to 'Edit', edit_admin_post_path(post) %></td>
18
+ <td><%= link_to 'Destroy', [:admin, post], confirm: 'Are you sure?', method: :delete %></td>
19
+ </tr>
20
+ <% end %>
21
+ </table>
22
+
23
+ <br />
24
+
25
+ <%= link_to 'New Post', new_admin_post_path %>
@@ -0,0 +1,5 @@
1
+ <h1>New post</h1>
2
+
3
+ <%= render 'form' %>
4
+
5
+ <%= link_to 'Back', admin_posts_path %>
@@ -0,0 +1,15 @@
1
+ <p id="notice"><%= notice %></p>
2
+
3
+ <p>
4
+ <b>Title:</b>
5
+ <%= @post.title %>
6
+ </p>
7
+
8
+ <p>
9
+ <b>Body:</b>
10
+ <%= @post.body %>
11
+ </p>
12
+
13
+
14
+ <%= link_to 'Edit', edit_admin_post_path(@post) %> |
15
+ <%= link_to 'Back', admin_posts_path %>
@@ -0,0 +1 @@
1
+ <h1>Hello world!</h1>
@@ -53,8 +53,6 @@ module Dummy
53
53
  # Enable escaping HTML in JSON.
54
54
  config.active_support.escape_html_entities_in_json = true
55
55
 
56
- config.active_record.legacy_connection_handling = false
57
-
58
56
  # Use SQL instead of Active Record's schema dumper when creating the database.
59
57
  # This is necessary if your schema can't be completely dumped by the schema dumper,
60
58
  # like if you have constraints or database-specific column types
@@ -1,6 +1,9 @@
1
1
  Dummy::Application.routes.draw do
2
2
  devise_for :users
3
+ devise_for :admins
3
4
 
4
5
  resources :posts
5
- root to: "posts#index"
6
+ resources :admin_posts
7
+
8
+ root to: "base#home"
6
9
  end
@@ -0,0 +1,9 @@
1
+ class CreateAdmins < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :admins do |t|
4
+ t.string :name
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,52 @@
1
+ class AddDeviseToAdmins < ActiveRecord::Migration[5.0]
2
+ def self.up
3
+ change_table(:admins) do |t|
4
+ ## Database authenticatable
5
+ t.string :email, null: false, default: ""
6
+ t.string :encrypted_password, null: false, default: ""
7
+
8
+ ## Recoverable
9
+ t.string :reset_password_token
10
+ t.datetime :reset_password_sent_at
11
+
12
+ ## Rememberable
13
+ t.datetime :remember_created_at
14
+
15
+ ## Trackable
16
+ t.integer :sign_in_count, default: 0
17
+ t.datetime :current_sign_in_at
18
+ t.datetime :last_sign_in_at
19
+ t.string :current_sign_in_ip
20
+ t.string :last_sign_in_ip
21
+
22
+ ## Confirmable
23
+ # t.string :confirmation_token
24
+ # t.datetime :confirmed_at
25
+ # t.datetime :confirmation_sent_at
26
+ # t.string :unconfirmed_email # Only if using reconfirmable
27
+
28
+ ## Lockable
29
+ t.integer :failed_attempts, default: 0 # Only if lock strategy is :failed_attempts
30
+ t.string :unlock_token # Only if unlock strategy is :email or :both
31
+ t.datetime :locked_at
32
+
33
+ ## Token authenticatable
34
+ t.string :authentication_token
35
+
36
+ # Uncomment below if timestamps were not included in your original model.
37
+ # t.timestamps
38
+ end
39
+
40
+ add_index :admins, :email, unique: true
41
+ add_index :admins, :reset_password_token, unique: true
42
+ # add_index :admins, :confirmation_token, :unique => true
43
+ add_index :admins, :unlock_token, unique: true
44
+ add_index :admins, :authentication_token, unique: true
45
+ end
46
+
47
+ def self.down
48
+ # By default, we don't want to make any assumption about how to roll back a migration when your
49
+ # model already existed. Please edit below which fields you would like to remove in this migration.
50
+ raise ActiveRecord::IrreversibleMigration
51
+ end
52
+ end
@@ -0,0 +1,28 @@
1
+ class DeviseOtpAddToAdmins < ActiveRecord::Migration[5.0]
2
+ def self.up
3
+ change_table :admins do |t|
4
+ t.string :otp_auth_secret
5
+ t.string :otp_recovery_secret
6
+ t.boolean :otp_enabled, default: false, null: false
7
+ t.boolean :otp_mandatory, default: false, null: false
8
+ t.datetime :otp_enabled_on
9
+ t.integer :otp_time_drift, default: 0, null: false
10
+ t.integer :otp_failed_attempts, default: 0, null: false
11
+ t.integer :otp_recovery_counter, default: 0, null: false
12
+ t.string :otp_persistence_seed
13
+
14
+ t.string :otp_session_challenge
15
+ t.datetime :otp_challenge_expires
16
+ end
17
+
18
+ add_index :admins, :otp_session_challenge, unique: true
19
+ add_index :admins, :otp_challenge_expires
20
+ end
21
+
22
+ def self.down
23
+ change_table :admins do |t|
24
+ t.remove :otp_auth_secret, :otp_recovery_secret, :otp_enabled, :otp_mandatory, :otp_enabled_on, :otp_session_challenge,
25
+ :otp_challenge_expires, :otp_time_drift, :otp_failed_attempts, :otp_recovery_counter, :otp_persistence_seed
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,53 @@
1
+ require "test_helper"
2
+ require "integration_tests_helper"
3
+
4
+ class DisableTokenTest < ActionDispatch::IntegrationTest
5
+
6
+ def setup
7
+ # log in 1fa
8
+ @user = enable_otp_and_sign_in
9
+ assert_equal user_otp_credential_path, current_path
10
+
11
+ # otp 2fa
12
+ fill_in "token", with: ROTP::TOTP.new(@user.otp_auth_secret).at(Time.now)
13
+ click_button "Submit Token"
14
+ assert_equal root_path, current_path
15
+ end
16
+
17
+ def teardown
18
+ Capybara.reset_sessions!
19
+ end
20
+
21
+ test "disabling OTP after successfully enabling" do
22
+ # disable OTP
23
+ disable_otp
24
+
25
+ assert page.has_content? "Disabled"
26
+
27
+ # logout
28
+ sign_out
29
+
30
+ # log back in 1fa
31
+ sign_user_in(@user)
32
+
33
+ assert_equal root_path, current_path
34
+ end
35
+
36
+ test "disabling OTP does not reset token secrets" do
37
+ # get otp secrets
38
+ @user.reload
39
+ auth_secret = @user.otp_auth_secret
40
+ recovery_secret = @user.otp_recovery_secret
41
+
42
+ # disable OTP
43
+ disable_otp
44
+
45
+ # compare otp secrets
46
+ assert_not_nil @user.otp_auth_secret
47
+ assert_equal @user.otp_auth_secret, auth_secret
48
+
49
+ assert_not_nil @user.otp_recovery_secret
50
+ assert_equal @user.otp_recovery_secret, recovery_secret
51
+ end
52
+
53
+ end
@@ -0,0 +1,57 @@
1
+ require "test_helper"
2
+ require "integration_tests_helper"
3
+
4
+ class EnableOtpFormTest < ActionDispatch::IntegrationTest
5
+ def teardown
6
+ Capybara.reset_sessions!
7
+ end
8
+
9
+ test "a user should be able enable their OTP authentication by entering a confirmation code" do
10
+ user = sign_user_in
11
+
12
+ visit edit_user_otp_token_path
13
+
14
+ user.reload
15
+
16
+ fill_in "confirmation_code", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
17
+
18
+ click_button "Continue..."
19
+
20
+ assert_equal user_otp_token_path, current_path
21
+ assert page.has_content?("Enabled")
22
+
23
+ user.reload
24
+ assert user.otp_enabled?
25
+ end
26
+
27
+ test "a user should not be able enable their OTP authentication with an incorrect confirmation code" do
28
+ user = sign_user_in
29
+
30
+ visit edit_user_otp_token_path
31
+
32
+ fill_in "confirmation_code", with: "123456"
33
+
34
+ click_button "Continue..."
35
+
36
+ assert page.has_content?("To Enable Two-Factor Authentication")
37
+
38
+ user.reload
39
+ assert_not user.otp_enabled?
40
+ end
41
+
42
+ test "a user should not be able enable their OTP authentication with a blank confirmation code" do
43
+ user = sign_user_in
44
+
45
+ visit edit_user_otp_token_path
46
+
47
+ fill_in "confirmation_code", with: ""
48
+
49
+ click_button "Continue..."
50
+
51
+ assert page.has_content?("To Enable Two-Factor Authentication")
52
+
53
+ user.reload
54
+ assert_not user.otp_enabled?
55
+ end
56
+
57
+ end