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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +0 -2
- data/.gitignore +3 -1
- data/CHANGELOG.md +51 -8
- data/README.md +8 -2
- data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +46 -27
- data/app/controllers/devise_otp/devise/otp_tokens_controller.rb +24 -6
- data/app/views/devise/otp_credentials/show.html.erb +6 -6
- data/app/views/devise/otp_tokens/_token_secret.html.erb +3 -4
- data/app/views/devise/otp_tokens/edit.html.erb +26 -0
- data/app/views/devise/otp_tokens/show.html.erb +7 -14
- data/config/locales/en.yml +23 -14
- data/devise-otp.gemspec +1 -2
- data/lib/devise/strategies/database_authenticatable.rb +64 -0
- data/lib/devise-otp/version.rb +1 -1
- data/lib/devise-otp.rb +31 -11
- data/lib/devise_otp_authenticatable/controllers/helpers.rb +9 -10
- data/lib/devise_otp_authenticatable/controllers/public_helpers.rb +39 -0
- data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +10 -0
- data/lib/devise_otp_authenticatable/engine.rb +2 -5
- data/lib/devise_otp_authenticatable/hooks/refreshable.rb +5 -0
- data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +22 -20
- data/lib/devise_otp_authenticatable/routes.rb +3 -1
- data/test/dummy/app/controllers/admin_posts_controller.rb +85 -0
- data/test/dummy/app/controllers/application_controller.rb +0 -1
- data/test/dummy/app/controllers/base_controller.rb +6 -0
- data/test/dummy/app/models/admin.rb +25 -0
- data/test/dummy/app/views/admin_posts/_form.html.erb +25 -0
- data/test/dummy/app/views/admin_posts/edit.html.erb +6 -0
- data/test/dummy/app/views/admin_posts/index.html.erb +25 -0
- data/test/dummy/app/views/admin_posts/new.html.erb +5 -0
- data/test/dummy/app/views/admin_posts/show.html.erb +15 -0
- data/test/dummy/app/views/base/home.html.erb +1 -0
- data/test/dummy/config/application.rb +0 -2
- data/test/dummy/config/routes.rb +4 -1
- data/test/dummy/db/migrate/20240604000001_create_admins.rb +9 -0
- data/test/dummy/db/migrate/20240604000002_add_devise_to_admins.rb +52 -0
- data/test/dummy/db/migrate/20240604000003_devise_otp_add_to_admins.rb +28 -0
- data/test/integration/disable_token_test.rb +53 -0
- data/test/integration/enable_otp_form_test.rb +57 -0
- data/test/integration/persistence_test.rb +3 -6
- data/test/integration/refresh_test.rb +32 -0
- data/test/integration/reset_token_test.rb +45 -0
- data/test/integration/sign_in_test.rb +10 -14
- data/test/integration/trackable_test.rb +50 -0
- data/test/integration_tests_helper.rb +24 -6
- data/test/models/otp_authenticatable_test.rb +62 -27
- data/test/test_helper.rb +1 -71
- metadata +26 -23
- data/lib/devise_otp_authenticatable/hooks/sessions.rb +0 -58
- data/lib/devise_otp_authenticatable/hooks.rb +0 -11
- 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[
|
51
|
-
|
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[
|
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[
|
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(
|
91
|
+
warden.session(resource_name).delete(otp_refresh_return_url_property) { :root }
|
93
92
|
end
|
94
93
|
|
95
|
-
def
|
96
|
-
"
|
94
|
+
def otp_refresh_return_url_property
|
95
|
+
"refresh_return_url"
|
97
96
|
end
|
98
97
|
|
99
|
-
def
|
100
|
-
"
|
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
|
@@ -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
|
64
|
-
if otp_persistence_seed.
|
65
|
-
|
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
|
@@ -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,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 @@
|
|
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
|
data/test/dummy/config/routes.rb
CHANGED
@@ -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
|