devise-otp 0.6.0 → 0.7.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 (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