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.
- 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
|