devise-otp 0.2.2 → 0.4.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 +5 -5
- data/.github/workflows/ci.yml +36 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +1 -22
- data/README.md +42 -75
- data/app/assets/javascripts/devise-otp.js +1 -0
- data/app/assets/javascripts/qrcode.js +609 -0
- data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +102 -0
- data/app/controllers/devise_otp/devise/otp_tokens_controller.rb +112 -0
- data/app/views/devise/otp_credentials/refresh.html.erb +19 -0
- data/app/views/devise/otp_credentials/show.html.erb +31 -0
- data/app/views/devise/otp_tokens/_token_secret.html.erb +23 -0
- data/app/views/devise/otp_tokens/_trusted_devices.html.erb +12 -0
- data/app/views/devise/otp_tokens/recovery.html.erb +21 -0
- data/app/views/devise/otp_tokens/recovery_codes.text.erb +3 -0
- data/app/views/devise/otp_tokens/show.html.erb +21 -0
- data/config/locales/en.yml +8 -8
- data/devise-otp.gemspec +15 -10
- data/docs/QR_CODES.md +48 -0
- data/lib/devise-otp/version.rb +2 -2
- data/lib/devise-otp.rb +12 -11
- data/lib/devise_otp_authenticatable/controllers/helpers.rb +22 -15
- data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +6 -7
- data/lib/devise_otp_authenticatable/engine.rb +22 -13
- data/lib/devise_otp_authenticatable/hooks/sessions.rb +8 -7
- data/lib/devise_otp_authenticatable/hooks.rb +1 -1
- data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +14 -13
- data/lib/devise_otp_authenticatable/routes.rb +2 -5
- data/lib/generators/active_record/templates/migration.rb +1 -1
- data/lib/generators/devise_otp/install_generator.rb +8 -5
- data/lib/generators/devise_otp/views_generator.rb +2 -3
- data/test/dummy/app/assets/config/manifest.js +2 -0
- data/test/dummy/app/assets/javascripts/application.js +1 -0
- data/test/dummy/app/controllers/application_controller.rb +1 -1
- data/test/dummy/app/controllers/posts_controller.rb +2 -0
- data/test/dummy/app/models/user.rb +1 -1
- data/test/dummy/config/application.rb +2 -1
- data/test/dummy/config/database.yml +1 -1
- data/test/dummy/config/environments/development.rb +0 -7
- data/test/dummy/config/environments/production.rb +0 -4
- data/test/dummy/db/migrate/20130125101430_create_users.rb +1 -1
- data/test/dummy/db/migrate/20130131092406_add_devise_to_users.rb +1 -1
- data/test/dummy/db/migrate/20130131142320_create_posts.rb +1 -1
- data/test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb +2 -2
- data/test/dummy/script/rails +0 -0
- data/test/integration/persistence_test.rb +81 -0
- data/test/integration/refresh_test.rb +2 -18
- data/test/integration/sign_in_test.rb +13 -3
- data/test/integration/token_test.rb +1 -4
- data/test/integration_tests_helper.rb +7 -3
- data/test/models/otp_authenticatable_test.rb +14 -9
- data/test/orm/active_record.rb +3 -1
- data/test/test_helper.rb +71 -2
- metadata +132 -25
- data/.travis.yml +0 -12
- data/app/controllers/devise_otp/credentials_controller.rb +0 -106
- data/app/controllers/devise_otp/tokens_controller.rb +0 -105
- data/app/views/devise_otp/credentials/refresh.html.erb +0 -20
- data/app/views/devise_otp/credentials/show.html.erb +0 -23
- data/app/views/devise_otp/tokens/_token_secret.html.erb +0 -17
- data/app/views/devise_otp/tokens/_trusted_devices.html.erb +0 -10
- data/app/views/devise_otp/tokens/recovery.html.erb +0 -21
- data/app/views/devise_otp/tokens/show.html.erb +0 -19
- data/lib/devise_otp_authenticatable/mapping.rb +0 -19
@@ -1,33 +1,32 @@
|
|
1
1
|
module DeviseOtpAuthenticatable
|
2
2
|
module Controllers
|
3
|
-
|
4
3
|
module UrlHelpers
|
5
4
|
|
6
5
|
def recovery_otp_token_for(resource_or_scope, opts = {})
|
7
|
-
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
6
|
+
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
|
8
7
|
send("recovery_#{scope}_otp_token_path", opts)
|
9
8
|
end
|
10
9
|
|
11
10
|
def refresh_otp_credential_path_for(resource_or_scope, opts = {})
|
12
|
-
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
11
|
+
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
|
13
12
|
send("refresh_#{scope}_otp_credential_path", opts)
|
14
13
|
end
|
15
14
|
|
16
15
|
def persistence_otp_token_path_for(resource_or_scope, opts = {})
|
17
|
-
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
16
|
+
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
|
18
17
|
send("persistence_#{scope}_otp_token_path", opts)
|
19
18
|
end
|
20
19
|
|
21
20
|
def otp_token_path_for(resource_or_scope, opts = {})
|
22
|
-
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
21
|
+
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
|
23
22
|
send("#{scope}_otp_token_path", opts)
|
24
23
|
end
|
25
24
|
|
26
25
|
def otp_credential_path_for(resource_or_scope, opts = {})
|
27
|
-
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
26
|
+
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
|
28
27
|
send("#{scope}_otp_credential_path", opts)
|
29
28
|
end
|
30
29
|
|
31
30
|
end
|
32
31
|
end
|
33
|
-
end
|
32
|
+
end
|
@@ -1,23 +1,32 @@
|
|
1
1
|
module DeviseOtpAuthenticatable
|
2
2
|
class Engine < ::Rails::Engine
|
3
3
|
|
4
|
-
ActiveSupport.on_load(:action_controller) do
|
5
|
-
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
|
6
|
-
include DeviseOtpAuthenticatable::Controllers::Helpers
|
7
|
-
end
|
8
|
-
ActiveSupport.on_load(:action_view) do
|
9
|
-
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
|
10
|
-
include DeviseOtpAuthenticatable::Controllers::Helpers
|
11
|
-
end
|
12
|
-
|
13
4
|
# We use to_prepare instead of after_initialize here because Devise is a Rails engine;
|
14
5
|
config.to_prepare do
|
15
6
|
DeviseOtpAuthenticatable::Hooks.apply
|
16
7
|
end
|
17
8
|
|
18
|
-
|
19
|
-
|
20
|
-
|
9
|
+
initializer "devise-otp", group: :all do |app|
|
10
|
+
ActiveSupport.on_load(:devise_controller) do
|
11
|
+
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
|
12
|
+
include DeviseOtpAuthenticatable::Controllers::Helpers
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveSupport.on_load(:action_view) do
|
16
|
+
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
|
17
|
+
include DeviseOtpAuthenticatable::Controllers::Helpers
|
18
|
+
end
|
19
|
+
|
20
|
+
# See: https://guides.rubyonrails.org/engines.html#separate-assets-and-precompiling
|
21
|
+
# check if Rails api mode
|
22
|
+
if app.config.respond_to?(:assets)
|
23
|
+
if defined?(Sprockets) && Sprockets::VERSION >= "4"
|
24
|
+
app.config.assets.precompile << "devise-otp.js"
|
25
|
+
else
|
26
|
+
# use a proc instead of a string
|
27
|
+
app.config.assets.precompile << proc { |path| path == "devise-otp.js" }
|
28
|
+
end
|
29
|
+
end
|
21
30
|
end
|
22
31
|
end
|
23
|
-
end
|
32
|
+
end
|
@@ -4,36 +4,36 @@ module DeviseOtpAuthenticatable::Hooks
|
|
4
4
|
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
|
5
5
|
|
6
6
|
included do
|
7
|
-
|
7
|
+
alias_method :create, :create_with_otp
|
8
8
|
end
|
9
9
|
|
10
10
|
#
|
11
11
|
# replaces Devise::SessionsController#create
|
12
12
|
#
|
13
13
|
def create_with_otp
|
14
|
-
|
15
14
|
resource = warden.authenticate!(auth_options)
|
16
15
|
|
16
|
+
devise_stored_location = stored_location_for(resource) # Grab the current stored location before it gets lost by warden.logout
|
17
|
+
store_location_for(resource, devise_stored_location) # Restore it since #stored_location_for removes it
|
18
|
+
|
17
19
|
otp_refresh_credentials_for(resource)
|
18
20
|
|
19
21
|
if otp_challenge_required_on?(resource)
|
20
22
|
challenge = resource.generate_otp_challenge!
|
21
23
|
warden.logout
|
24
|
+
store_location_for(resource, devise_stored_location) # restore the stored location
|
22
25
|
respond_with resource, :location => otp_credential_path_for(resource, {:challenge => challenge})
|
23
|
-
|
24
26
|
elsif otp_mandatory_on?(resource) # if mandatory, log in user but send him to the must activate otp
|
25
27
|
set_flash_message(:notice, :signed_in_but_otp) if is_navigational_format?
|
26
28
|
sign_in(resource_name, resource)
|
27
29
|
respond_with resource, :location => otp_token_path_for(resource)
|
28
30
|
else
|
29
|
-
|
30
31
|
set_flash_message(:notice, :signed_in) if is_navigational_format?
|
31
32
|
sign_in(resource_name, resource)
|
32
33
|
respond_with resource, :location => after_sign_in_path_for(resource)
|
33
34
|
end
|
34
35
|
end
|
35
36
|
|
36
|
-
|
37
37
|
private
|
38
38
|
|
39
39
|
#
|
@@ -41,7 +41,8 @@ module DeviseOtpAuthenticatable::Hooks
|
|
41
41
|
#
|
42
42
|
def otp_challenge_required_on?(resource)
|
43
43
|
return false unless resource.respond_to?(:otp_enabled) && resource.respond_to?(:otp_auth_secret)
|
44
|
-
|
44
|
+
|
45
|
+
resource.otp_enabled && !is_otp_trusted_browser_for?(resource)
|
45
46
|
end
|
46
47
|
|
47
48
|
#
|
@@ -54,4 +55,4 @@ module DeviseOtpAuthenticatable::Hooks
|
|
54
55
|
resource.otp_mandatory && !resource.otp_enabled
|
55
56
|
end
|
56
57
|
end
|
57
|
-
end
|
58
|
+
end
|
@@ -12,7 +12,8 @@ module Devise::Models
|
|
12
12
|
|
13
13
|
module ClassMethods
|
14
14
|
::Devise::Models.config(self, :otp_authentication_timeout, :otp_drift_window, :otp_trust_persistence,
|
15
|
-
:otp_mandatory, :otp_credentials_refresh, :
|
15
|
+
:otp_mandatory, :otp_credentials_refresh, :otp_issuer, :otp_recovery_tokens,
|
16
|
+
:otp_controller_path)
|
16
17
|
|
17
18
|
def find_valid_otp_challenge(challenge)
|
18
19
|
with_valid_otp_challenge(Time.now).where(:otp_session_challenge => challenge).first
|
@@ -20,7 +21,7 @@ module Devise::Models
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def time_based_otp
|
23
|
-
@time_based_otp ||= ROTP::TOTP.new(otp_auth_secret)
|
24
|
+
@time_based_otp ||= ROTP::TOTP.new(otp_auth_secret, issuer: "#{self.class.otp_issuer || Rails.application.class.module_parent_name}")
|
24
25
|
end
|
25
26
|
|
26
27
|
def recovery_otp
|
@@ -32,7 +33,7 @@ module Devise::Models
|
|
32
33
|
end
|
33
34
|
|
34
35
|
def otp_provisioning_identifier
|
35
|
-
|
36
|
+
email
|
36
37
|
end
|
37
38
|
|
38
39
|
|
@@ -41,7 +42,7 @@ module Devise::Models
|
|
41
42
|
@recovery_otp = nil
|
42
43
|
generate_otp_auth_secret
|
43
44
|
reset_otp_persistence
|
44
|
-
update(:otp_enabled => false,
|
45
|
+
update!(:otp_enabled => false,
|
45
46
|
:otp_session_challenge => nil, :otp_challenge_expires => nil,
|
46
47
|
:otp_recovery_counter => 0)
|
47
48
|
end
|
@@ -61,11 +62,15 @@ module Devise::Models
|
|
61
62
|
end
|
62
63
|
|
63
64
|
def enable_otp!
|
65
|
+
if otp_persistence_seed.nil?
|
66
|
+
reset_otp_credentials!
|
67
|
+
end
|
68
|
+
|
64
69
|
update!(:otp_enabled => true, :otp_enabled_on => Time.now)
|
65
70
|
end
|
66
71
|
|
67
72
|
def disable_otp!
|
68
|
-
update!(:otp_enabled => false, :otp_enabled_on => nil
|
73
|
+
update!(:otp_enabled => false, :otp_enabled_on => nil)
|
69
74
|
end
|
70
75
|
|
71
76
|
def generate_otp_challenge!(expires = nil)
|
@@ -89,12 +94,8 @@ module Devise::Models
|
|
89
94
|
alias_method :valid_otp_token?, :validate_otp_token
|
90
95
|
|
91
96
|
def validate_otp_time_token(token)
|
92
|
-
|
93
|
-
|
94
|
-
true
|
95
|
-
else
|
96
|
-
false
|
97
|
-
end
|
97
|
+
return false if token.blank?
|
98
|
+
validate_otp_token_with_drift(token)
|
98
99
|
end
|
99
100
|
alias_method :valid_otp_time_token?, :validate_otp_time_token
|
100
101
|
|
@@ -121,7 +122,7 @@ module Devise::Models
|
|
121
122
|
|
122
123
|
# should be centered around saved drift
|
123
124
|
(-self.class.otp_drift_window..self.class.otp_drift_window).any? {|drift|
|
124
|
-
(time_based_otp.verify(token, Time.now.ago(30 * drift))) }
|
125
|
+
(time_based_otp.verify(token, at: Time.now.ago(30 * drift))) }
|
125
126
|
end
|
126
127
|
|
127
128
|
def generate_otp_persistence_seed
|
@@ -134,4 +135,4 @@ module Devise::Models
|
|
134
135
|
end
|
135
136
|
|
136
137
|
end
|
137
|
-
end
|
138
|
+
end
|
@@ -1,12 +1,9 @@
|
|
1
1
|
module ActionDispatch::Routing
|
2
2
|
class Mapper
|
3
3
|
|
4
|
-
|
5
4
|
protected
|
6
|
-
#########
|
7
5
|
|
8
6
|
def devise_otp(mapping, controllers)
|
9
|
-
|
10
7
|
namespace :otp, :module => :devise_otp do
|
11
8
|
resource :token, :only => [:show, :update, :destroy],
|
12
9
|
:path => mapping.path_names[:token], :controller => controllers[:otp_tokens] do
|
@@ -23,10 +20,10 @@ module ActionDispatch::Routing
|
|
23
20
|
resource :credential, :only => [:show, :update],
|
24
21
|
:path => mapping.path_names[:credentials], :controller => controllers[:otp_credentials] do
|
25
22
|
|
26
|
-
get
|
23
|
+
get :refresh, :action => 'get_refresh'
|
27
24
|
put :refresh, :action => 'set_refresh'
|
28
25
|
end
|
29
26
|
end
|
30
27
|
end
|
31
28
|
end
|
32
|
-
end
|
29
|
+
end
|
@@ -20,7 +20,7 @@ class DeviseOtpAddTo<%= table_name.camelize %> < ActiveRecord::Migration
|
|
20
20
|
def self.down
|
21
21
|
change_table :<%= table_name %> do |t|
|
22
22
|
t.remove :otp_auth_secret, :otp_recovery_secret, :otp_enabled, :otp_mandatory, :otp_enabled_on, :otp_session_challenge,
|
23
|
-
:otp_challenge_expires, :
|
23
|
+
:otp_challenge_expires, :otp_failed_attempts, :otp_recovery_counter, :otp_persistence_seed
|
24
24
|
|
25
25
|
end
|
26
26
|
end
|
@@ -29,16 +29,19 @@ content = <<-CONTENT
|
|
29
29
|
|
30
30
|
# Users are given a list of one-time recovery tokens, for emergency access
|
31
31
|
# set to false to disable giving recovery tokens.
|
32
|
-
#config.
|
32
|
+
#config.otp_recovery_tokens = 10
|
33
33
|
|
34
34
|
# The user is allowed to set his browser as "trusted", no more OTP challenges will be
|
35
35
|
# asked for that browser, for a limited time.
|
36
36
|
# set to false to disable setting the browser as trusted
|
37
37
|
#config.otp_trust_persistence = 1.month
|
38
38
|
|
39
|
-
# The name of
|
40
|
-
# url
|
41
|
-
#config.
|
39
|
+
# The name of the token issuer, to be added to the provisioning
|
40
|
+
# url. Display will vary based on token application. (defaults to the Rails application class)
|
41
|
+
#config.otp_issuer = 'my_application'
|
42
|
+
|
43
|
+
# Custom view path for Devise OTP controllers
|
44
|
+
#config.otp_controller_path = 'devise'
|
42
45
|
|
43
46
|
CONTENT
|
44
47
|
|
@@ -50,4 +53,4 @@ CONTENT
|
|
50
53
|
end
|
51
54
|
end
|
52
55
|
end
|
53
|
-
end
|
56
|
+
end
|
@@ -9,10 +9,9 @@ module DeviseOtp
|
|
9
9
|
:desc => "The scope to copy views to"
|
10
10
|
|
11
11
|
include ::Devise::Generators::ViewPathTemplates
|
12
|
-
source_root File.expand_path("../../../../app/views
|
12
|
+
source_root File.expand_path("../../../../app/views", __FILE__)
|
13
13
|
def copy_views
|
14
|
-
view_directory :
|
15
|
-
view_directory :tokens, 'app/views/devise_otp/tokens'
|
14
|
+
view_directory :devise, 'app/views/devise'
|
16
15
|
end
|
17
16
|
end
|
18
17
|
end
|
@@ -12,7 +12,7 @@ class User < PARENT_MODEL_CLASS
|
|
12
12
|
end
|
13
13
|
|
14
14
|
devise :otp_authenticatable, :database_authenticatable, :registerable,
|
15
|
-
:
|
15
|
+
:trackable, :validatable
|
16
16
|
|
17
17
|
# Setup accessible (or protected) attributes for your model
|
18
18
|
#attr_accessible :otp_enabled, :otp_mandatory, :as => :otp_privileged
|
@@ -53,6 +53,8 @@ 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
|
+
|
56
58
|
# Use SQL instead of Active Record's schema dumper when creating the database.
|
57
59
|
# This is necessary if your schema can't be completely dumped by the schema dumper,
|
58
60
|
# like if you have constraints or database-specific column types
|
@@ -65,4 +67,3 @@ module Dummy
|
|
65
67
|
config.assets.version = '1.0'
|
66
68
|
end
|
67
69
|
end
|
68
|
-
|
@@ -22,13 +22,6 @@ Dummy::Application.configure do
|
|
22
22
|
# Only use best-standards-support built into browsers
|
23
23
|
config.action_dispatch.best_standards_support = :builtin
|
24
24
|
|
25
|
-
# Raise exception on mass assignment protection for Active Record models
|
26
|
-
config.active_record.mass_assignment_sanitizer = :strict
|
27
|
-
|
28
|
-
# Log the query plan for queries taking more than this (works
|
29
|
-
# with SQLite, MySQL, and PostgreSQL)
|
30
|
-
config.active_record.auto_explain_threshold_in_seconds = 0.5
|
31
|
-
|
32
25
|
# Do not compress assets
|
33
26
|
config.assets.compress = false
|
34
27
|
|
@@ -66,8 +66,4 @@ Dummy::Application.configure do
|
|
66
66
|
|
67
67
|
# Send deprecation notices to registered listeners
|
68
68
|
config.active_support.deprecation = :notify
|
69
|
-
|
70
|
-
# Log the query plan for queries taking more than this (works
|
71
|
-
# with SQLite, MySQL, and PostgreSQL)
|
72
|
-
# config.active_record.auto_explain_threshold_in_seconds = 0.5
|
73
69
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class DeviseOtpAddToUsers < ActiveRecord::Migration
|
1
|
+
class DeviseOtpAddToUsers < ActiveRecord::Migration[5.0]
|
2
2
|
def self.up
|
3
3
|
change_table :users do |t|
|
4
4
|
t.string :otp_auth_secret
|
@@ -18,7 +18,7 @@ class DeviseOtpAddToUsers < ActiveRecord::Migration
|
|
18
18
|
add_index :users, :otp_session_challenge, :unique => true
|
19
19
|
add_index :users, :otp_challenge_expires
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def self.down
|
23
23
|
change_table :users do |t|
|
24
24
|
t.remove :otp_auth_secret, :otp_recovery_secret, :otp_enabled, :otp_mandatory, :otp_enabled_on, :otp_session_challenge,
|
data/test/dummy/script/rails
CHANGED
File without changes
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'integration_tests_helper'
|
3
|
+
|
4
|
+
class PersistenceTest < ActionDispatch::IntegrationTest
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@old_persistence = User.otp_trust_persistence
|
8
|
+
User.otp_trust_persistence = 3.seconds
|
9
|
+
end
|
10
|
+
|
11
|
+
def teardown
|
12
|
+
User.otp_trust_persistence = @old_persistence
|
13
|
+
Capybara.reset_sessions!
|
14
|
+
end
|
15
|
+
|
16
|
+
test 'a user should be requested the otp challenge every log in' do
|
17
|
+
# log in 1fa
|
18
|
+
user = enable_otp_and_sign_in
|
19
|
+
otp_challenge_for user
|
20
|
+
|
21
|
+
visit user_otp_token_path
|
22
|
+
assert_equal user_otp_token_path, current_path
|
23
|
+
|
24
|
+
sign_out
|
25
|
+
sign_user_in
|
26
|
+
|
27
|
+
assert_equal user_otp_credential_path, current_path
|
28
|
+
end
|
29
|
+
|
30
|
+
test 'a user should be able to set their browser as trusted' do
|
31
|
+
# log in 1fa
|
32
|
+
user = enable_otp_and_sign_in
|
33
|
+
otp_challenge_for user
|
34
|
+
|
35
|
+
visit user_otp_token_path
|
36
|
+
assert_equal user_otp_token_path, current_path
|
37
|
+
|
38
|
+
click_link('Trust this browser')
|
39
|
+
assert_text 'Your browser is trusted.'
|
40
|
+
sign_out
|
41
|
+
|
42
|
+
sign_user_in
|
43
|
+
|
44
|
+
assert_equal root_path, current_path
|
45
|
+
end
|
46
|
+
|
47
|
+
test 'a user should be able to download its recovery codes' do
|
48
|
+
# log in 1fa
|
49
|
+
user = enable_otp_and_sign_in
|
50
|
+
otp_challenge_for user
|
51
|
+
|
52
|
+
visit user_otp_token_path
|
53
|
+
assert_equal user_otp_token_path, current_path
|
54
|
+
|
55
|
+
enable_chrome_headless_downloads(page, "/tmp/devise-otp")
|
56
|
+
|
57
|
+
DownloadHelper.wait_for_download(count: 1) do
|
58
|
+
click_link('Download recovery codes')
|
59
|
+
end
|
60
|
+
|
61
|
+
assert_equal 1, DownloadHelper.downloads.size
|
62
|
+
end
|
63
|
+
|
64
|
+
test 'trusted status should expire' do
|
65
|
+
# log in 1fa
|
66
|
+
user = enable_otp_and_sign_in
|
67
|
+
otp_challenge_for user
|
68
|
+
|
69
|
+
visit user_otp_token_path
|
70
|
+
assert_equal user_otp_token_path, current_path
|
71
|
+
|
72
|
+
click_link('Trust this browser')
|
73
|
+
assert_text 'Your browser is trusted.'
|
74
|
+
sign_out
|
75
|
+
|
76
|
+
sleep User.otp_trust_persistence.to_i + 1
|
77
|
+
sign_user_in
|
78
|
+
|
79
|
+
assert_equal user_otp_credential_path, current_path
|
80
|
+
end
|
81
|
+
end
|
@@ -21,7 +21,6 @@ class RefreshTest < ActionDispatch::IntegrationTest
|
|
21
21
|
end
|
22
22
|
|
23
23
|
test 'a user should be prompted for credentials when the credentials_refresh time is expired' do
|
24
|
-
|
25
24
|
sign_user_in
|
26
25
|
visit user_otp_token_path
|
27
26
|
assert_equal user_otp_token_path, current_path
|
@@ -45,7 +44,6 @@ class RefreshTest < ActionDispatch::IntegrationTest
|
|
45
44
|
fill_in 'user_refresh_password', :with => '12345678'
|
46
45
|
click_button 'Continue...'
|
47
46
|
assert_equal user_otp_token_path, current_path
|
48
|
-
|
49
47
|
end
|
50
48
|
|
51
49
|
test 'a user should NOT be able to access their OTP settings unless refreshing' do
|
@@ -63,20 +61,7 @@ class RefreshTest < ActionDispatch::IntegrationTest
|
|
63
61
|
assert_equal refresh_user_otp_credential_path, current_path
|
64
62
|
end
|
65
63
|
|
66
|
-
test 'user should be
|
67
|
-
enable_otp_and_sign_in_with_otp
|
68
|
-
|
69
|
-
sleep(2)
|
70
|
-
visit user_otp_token_path
|
71
|
-
assert_equal refresh_user_otp_credential_path, current_path
|
72
|
-
|
73
|
-
fill_in 'user_refresh_password', :with => '12345678'
|
74
|
-
click_button 'Continue...'
|
75
|
-
|
76
|
-
assert_equal refresh_user_otp_credential_path, current_path
|
77
|
-
end
|
78
|
-
|
79
|
-
test 'user should be finally be able to access their settings, if they provide both a password and a valid OTP token' do
|
64
|
+
test 'user should be finally be able to access their settings, and just password is enough' do
|
80
65
|
user = enable_otp_and_sign_in_with_otp
|
81
66
|
|
82
67
|
sleep(2)
|
@@ -84,9 +69,8 @@ class RefreshTest < ActionDispatch::IntegrationTest
|
|
84
69
|
assert_equal refresh_user_otp_credential_path, current_path
|
85
70
|
|
86
71
|
fill_in 'user_refresh_password', :with => '12345678'
|
87
|
-
fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
88
72
|
click_button 'Continue...'
|
89
73
|
|
90
74
|
assert_equal user_otp_token_path, current_path
|
91
75
|
end
|
92
|
-
end
|
76
|
+
end
|
@@ -10,12 +10,12 @@ class SignInTest < ActionDispatch::IntegrationTest
|
|
10
10
|
test 'a new user should be able to sign in without using their token' do
|
11
11
|
create_full_user
|
12
12
|
|
13
|
-
visit
|
13
|
+
visit posts_path
|
14
14
|
fill_in 'user_email', :with => 'user@email.invalid'
|
15
15
|
fill_in 'user_password', :with => '12345678'
|
16
16
|
page.has_content?('Log in') ? click_button('Log in') : click_button('Sign in')
|
17
17
|
|
18
|
-
assert_equal
|
18
|
+
assert_equal posts_path, current_path
|
19
19
|
end
|
20
20
|
|
21
21
|
test 'a new user, just signed in, should be able to sign in and enable their OTP authentication' do
|
@@ -50,6 +50,16 @@ class SignInTest < ActionDispatch::IntegrationTest
|
|
50
50
|
assert_equal new_user_session_path, current_path
|
51
51
|
end
|
52
52
|
|
53
|
+
test 'fail blank token authentication' do
|
54
|
+
enable_otp_and_sign_in
|
55
|
+
assert_equal user_otp_credential_path, current_path
|
56
|
+
|
57
|
+
fill_in 'user_token', :with => ''
|
58
|
+
click_button 'Submit Token'
|
59
|
+
|
60
|
+
assert_equal user_otp_credential_path, current_path
|
61
|
+
end
|
62
|
+
|
53
63
|
test 'successful token authentication' do
|
54
64
|
user = enable_otp_and_sign_in
|
55
65
|
|
@@ -74,4 +84,4 @@ class SignInTest < ActionDispatch::IntegrationTest
|
|
74
84
|
User.otp_authentication_timeout = old_timeout
|
75
85
|
assert_equal new_user_session_path, current_path
|
76
86
|
end
|
77
|
-
end
|
87
|
+
end
|
@@ -3,13 +3,11 @@ require 'integration_tests_helper'
|
|
3
3
|
|
4
4
|
class TokenTest < ActionDispatch::IntegrationTest
|
5
5
|
|
6
|
-
|
7
6
|
def teardown
|
8
7
|
Capybara.reset_sessions!
|
9
8
|
end
|
10
9
|
|
11
10
|
test 'disabling OTP after successfully enabling' do
|
12
|
-
|
13
11
|
# log in 1fa
|
14
12
|
user = enable_otp_and_sign_in
|
15
13
|
assert_equal user_otp_credential_path, current_path
|
@@ -29,6 +27,5 @@ class TokenTest < ActionDispatch::IntegrationTest
|
|
29
27
|
sign_user_in(user)
|
30
28
|
|
31
29
|
assert_equal root_path, current_path
|
32
|
-
|
33
30
|
end
|
34
|
-
end
|
31
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
class ActionDispatch::IntegrationTest
|
2
|
+
include Warden::Test::Helpers
|
2
3
|
|
3
4
|
def warden
|
4
5
|
request.env['warden']
|
@@ -22,7 +23,6 @@ class ActionDispatch::IntegrationTest
|
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
25
|
-
|
26
26
|
def enable_otp_and_sign_in
|
27
27
|
user = create_full_user
|
28
28
|
sign_user_in(user)
|
@@ -36,6 +36,11 @@ class ActionDispatch::IntegrationTest
|
|
36
36
|
user
|
37
37
|
end
|
38
38
|
|
39
|
+
def otp_challenge_for(user)
|
40
|
+
fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
41
|
+
click_button 'Submit Token'
|
42
|
+
end
|
43
|
+
|
39
44
|
def disable_otp
|
40
45
|
visit user_otp_token_path
|
41
46
|
uncheck 'user_otp_enabled'
|
@@ -43,7 +48,7 @@ class ActionDispatch::IntegrationTest
|
|
43
48
|
end
|
44
49
|
|
45
50
|
def sign_out
|
46
|
-
|
51
|
+
logout :user
|
47
52
|
end
|
48
53
|
|
49
54
|
def sign_user_in(user = nil)
|
@@ -57,5 +62,4 @@ class ActionDispatch::IntegrationTest
|
|
57
62
|
user
|
58
63
|
end
|
59
64
|
|
60
|
-
|
61
65
|
end
|