devise-otp 0.1.1
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 +7 -0
- data/.gitignore +42 -0
- data/.travis.yml +11 -0
- data/Gemfile +25 -0
- data/LICENSE.txt +22 -0
- data/README.md +124 -0
- data/Rakefile +42 -0
- data/app/controllers/devise_otp/credentials_controller.rb +106 -0
- data/app/controllers/devise_otp/tokens_controller.rb +105 -0
- data/app/views/devise_otp/credentials/refresh.html.erb +20 -0
- data/app/views/devise_otp/credentials/show.html.erb +23 -0
- data/app/views/devise_otp/tokens/_token_secret.html.erb +17 -0
- data/app/views/devise_otp/tokens/recovery.html.erb +21 -0
- data/app/views/devise_otp/tokens/show.html.erb +31 -0
- data/config/locales/en.yml +66 -0
- data/devise-otp.gemspec +25 -0
- data/lib/devise-otp.rb +76 -0
- data/lib/devise-otp/version.rb +5 -0
- data/lib/devise_otp_authenticatable/controllers/helpers.rb +144 -0
- data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +35 -0
- data/lib/devise_otp_authenticatable/engine.rb +23 -0
- data/lib/devise_otp_authenticatable/hooks.rb +13 -0
- data/lib/devise_otp_authenticatable/hooks/sessions.rb +57 -0
- data/lib/devise_otp_authenticatable/mapping.rb +19 -0
- data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +140 -0
- data/lib/devise_otp_authenticatable/routes.rb +30 -0
- data/lib/generators/active_record/devise_otp_generator.rb +13 -0
- data/lib/generators/active_record/templates/migration.rb +28 -0
- data/lib/generators/devise_otp/devise_otp_generator.rb +17 -0
- data/lib/generators/devise_otp/install_generator.rb +31 -0
- data/lib/generators/devise_otp/views_generator.rb +19 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +4 -0
- data/test/dummy/app/controllers/posts_controller.rb +83 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/helpers/posts_helper.rb +2 -0
- data/test/dummy/app/mailers/.gitkeep +0 -0
- data/test/dummy/app/models/post.rb +2 -0
- data/test/dummy/app/models/user.rb +20 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/app/views/posts/_form.html.erb +25 -0
- data/test/dummy/app/views/posts/edit.html.erb +6 -0
- data/test/dummy/app/views/posts/index.html.erb +25 -0
- data/test/dummy/app/views/posts/new.html.erb +5 -0
- data/test/dummy/app/views/posts/show.html.erb +15 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +68 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +73 -0
- data/test/dummy/config/environments/test.rb +36 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/devise.rb +253 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +8 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +6 -0
- data/test/dummy/db/migrate/20130125101430_create_users.rb +9 -0
- data/test/dummy/db/migrate/20130131092406_add_devise_to_users.rb +53 -0
- data/test/dummy/db/migrate/20130131142320_create_posts.rb +10 -0
- data/test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb +28 -0
- data/test/dummy/lib/assets/.gitkeep +0 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/integration/refresh_test.rb +92 -0
- data/test/integration/sign_in_test.rb +77 -0
- data/test/integration_tests_helper.rb +48 -0
- data/test/model_tests_helper.rb +22 -0
- data/test/models/otp_authenticatable_test.rb +116 -0
- data/test/orm/active_record.rb +4 -0
- data/test/test_helper.rb +19 -0
- metadata +237 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
class DeviseOtpAddToUsers < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
change_table :users 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 :users, :otp_session_challenge, :unique => true
|
19
|
+
add_index :users, :otp_challenge_expires
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.down
|
23
|
+
change_table :users 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
|
File without changes
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>The page you were looking for doesn't exist (404)</title>
|
5
|
+
<style type="text/css">
|
6
|
+
body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
|
7
|
+
div.dialog {
|
8
|
+
width: 25em;
|
9
|
+
padding: 0 4em;
|
10
|
+
margin: 4em auto 0 auto;
|
11
|
+
border: 1px solid #ccc;
|
12
|
+
border-right-color: #999;
|
13
|
+
border-bottom-color: #999;
|
14
|
+
}
|
15
|
+
h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
|
16
|
+
</style>
|
17
|
+
</head>
|
18
|
+
|
19
|
+
<body>
|
20
|
+
<!-- This file lives in public/404.html -->
|
21
|
+
<div class="dialog">
|
22
|
+
<h1>The page you were looking for doesn't exist.</h1>
|
23
|
+
<p>You may have mistyped the address or the page may have moved.</p>
|
24
|
+
</div>
|
25
|
+
</body>
|
26
|
+
</html>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>The change you wanted was rejected (422)</title>
|
5
|
+
<style type="text/css">
|
6
|
+
body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
|
7
|
+
div.dialog {
|
8
|
+
width: 25em;
|
9
|
+
padding: 0 4em;
|
10
|
+
margin: 4em auto 0 auto;
|
11
|
+
border: 1px solid #ccc;
|
12
|
+
border-right-color: #999;
|
13
|
+
border-bottom-color: #999;
|
14
|
+
}
|
15
|
+
h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
|
16
|
+
</style>
|
17
|
+
</head>
|
18
|
+
|
19
|
+
<body>
|
20
|
+
<!-- This file lives in public/422.html -->
|
21
|
+
<div class="dialog">
|
22
|
+
<h1>The change you wanted was rejected.</h1>
|
23
|
+
<p>Maybe you tried to change something you didn't have access to.</p>
|
24
|
+
</div>
|
25
|
+
</body>
|
26
|
+
</html>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>We're sorry, but something went wrong (500)</title>
|
5
|
+
<style type="text/css">
|
6
|
+
body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
|
7
|
+
div.dialog {
|
8
|
+
width: 25em;
|
9
|
+
padding: 0 4em;
|
10
|
+
margin: 4em auto 0 auto;
|
11
|
+
border: 1px solid #ccc;
|
12
|
+
border-right-color: #999;
|
13
|
+
border-bottom-color: #999;
|
14
|
+
}
|
15
|
+
h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
|
16
|
+
</style>
|
17
|
+
</head>
|
18
|
+
|
19
|
+
<body>
|
20
|
+
<!-- This file lives in public/500.html -->
|
21
|
+
<div class="dialog">
|
22
|
+
<h1>We're sorry, but something went wrong.</h1>
|
23
|
+
</div>
|
24
|
+
</body>
|
25
|
+
</html>
|
File without changes
|
@@ -0,0 +1,6 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
|
3
|
+
|
4
|
+
APP_PATH = File.expand_path('../../config/application', __FILE__)
|
5
|
+
require File.expand_path('../../config/boot', __FILE__)
|
6
|
+
require 'rails/commands'
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'integration_tests_helper'
|
3
|
+
|
4
|
+
class RefreshTest < ActionDispatch::IntegrationTest
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@old_refresh = User.otp_credentials_refresh
|
8
|
+
User.otp_credentials_refresh = 1.second
|
9
|
+
end
|
10
|
+
|
11
|
+
def teardown
|
12
|
+
User.otp_credentials_refresh = @old_refresh
|
13
|
+
Capybara.reset_sessions!
|
14
|
+
end
|
15
|
+
|
16
|
+
test 'a user that just signed in should be able to access their OTP settings without refreshing' do
|
17
|
+
sign_user_in
|
18
|
+
|
19
|
+
visit user_otp_token_path
|
20
|
+
assert_equal user_otp_token_path, current_path
|
21
|
+
end
|
22
|
+
|
23
|
+
test 'a user should be prompted for credentials when the credentials_refresh time is expired' do
|
24
|
+
|
25
|
+
sign_user_in
|
26
|
+
visit user_otp_token_path
|
27
|
+
assert_equal user_otp_token_path, current_path
|
28
|
+
|
29
|
+
sleep(2)
|
30
|
+
|
31
|
+
visit user_otp_token_path
|
32
|
+
assert_equal refresh_user_otp_credential_path, current_path
|
33
|
+
end
|
34
|
+
|
35
|
+
test 'a user should be able to access their OTP settings after refreshing' do
|
36
|
+
sign_user_in
|
37
|
+
visit user_otp_token_path
|
38
|
+
assert_equal user_otp_token_path, current_path
|
39
|
+
|
40
|
+
sleep(2)
|
41
|
+
|
42
|
+
visit user_otp_token_path
|
43
|
+
assert_equal refresh_user_otp_credential_path, current_path
|
44
|
+
|
45
|
+
fill_in 'user_refresh_password', :with => '12345678'
|
46
|
+
click_button 'Continue...'
|
47
|
+
assert_equal user_otp_token_path, current_path
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
test 'a user should NOT be able to access their OTP settings unless refreshing' do
|
52
|
+
sign_user_in
|
53
|
+
visit user_otp_token_path
|
54
|
+
assert_equal user_otp_token_path, current_path
|
55
|
+
|
56
|
+
sleep(2)
|
57
|
+
|
58
|
+
visit user_otp_token_path
|
59
|
+
assert_equal refresh_user_otp_credential_path, current_path
|
60
|
+
|
61
|
+
fill_in 'user_refresh_password', :with => '12345670'
|
62
|
+
click_button 'Continue...'
|
63
|
+
assert_equal refresh_user_otp_credential_path, current_path
|
64
|
+
end
|
65
|
+
|
66
|
+
test 'user should be asked their OTP challenge in order to refresh, if they have OTP' do
|
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
|
80
|
+
user = enable_otp_and_sign_in_with_otp
|
81
|
+
|
82
|
+
sleep(2)
|
83
|
+
visit user_otp_token_path
|
84
|
+
assert_equal refresh_user_otp_credential_path, current_path
|
85
|
+
|
86
|
+
fill_in 'user_refresh_password', :with => '12345678'
|
87
|
+
fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
88
|
+
click_button 'Continue...'
|
89
|
+
|
90
|
+
assert_equal user_otp_token_path, current_path
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'integration_tests_helper'
|
3
|
+
|
4
|
+
class SignInTest < ActionDispatch::IntegrationTest
|
5
|
+
|
6
|
+
def teardown
|
7
|
+
Capybara.reset_sessions!
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'a new user should be able to sign in without using their token' do
|
11
|
+
create_full_user
|
12
|
+
|
13
|
+
visit new_user_session_path
|
14
|
+
fill_in 'user_email', :with => 'user@email.invalid'
|
15
|
+
fill_in 'user_password', :with => '12345678'
|
16
|
+
click_button 'Sign in'
|
17
|
+
|
18
|
+
assert_equal root_path, current_path
|
19
|
+
end
|
20
|
+
|
21
|
+
test 'a new user, just signed in, should be able to sign in and enable their OTP authentication' do
|
22
|
+
user = sign_user_in
|
23
|
+
|
24
|
+
visit user_otp_token_path
|
25
|
+
assert !page.has_content?('Your token secret')
|
26
|
+
|
27
|
+
check 'user_otp_enabled'
|
28
|
+
click_button 'Continue...'
|
29
|
+
|
30
|
+
assert_equal user_otp_token_path, current_path
|
31
|
+
|
32
|
+
assert page.has_content?('Your token secret')
|
33
|
+
assert !user.otp_auth_secret.nil?
|
34
|
+
assert !user.otp_persistence_seed.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
test 'a new user should be able to sign in enable OTP and be prompted for their token' do
|
38
|
+
enable_otp_and_sign_in
|
39
|
+
|
40
|
+
assert_equal user_otp_credential_path, current_path
|
41
|
+
end
|
42
|
+
|
43
|
+
test 'fail token authentication' do
|
44
|
+
enable_otp_and_sign_in
|
45
|
+
assert_equal user_otp_credential_path, current_path
|
46
|
+
|
47
|
+
fill_in 'user_token', :with => '123456'
|
48
|
+
click_button 'Submit Token'
|
49
|
+
|
50
|
+
assert_equal new_user_session_path, current_path
|
51
|
+
end
|
52
|
+
|
53
|
+
test 'successful token authentication' do
|
54
|
+
user = enable_otp_and_sign_in
|
55
|
+
|
56
|
+
fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
57
|
+
click_button 'Submit Token'
|
58
|
+
|
59
|
+
assert_equal root_path, current_path
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
test 'should fail if the the challenge times out' do
|
64
|
+
old_timeout = User.otp_authentication_timeout
|
65
|
+
User.otp_authentication_timeout = 1.second
|
66
|
+
|
67
|
+
user = enable_otp_and_sign_in
|
68
|
+
|
69
|
+
sleep(2)
|
70
|
+
|
71
|
+
fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
72
|
+
click_button 'Submit Token'
|
73
|
+
|
74
|
+
User.otp_authentication_timeout = old_timeout
|
75
|
+
assert_equal new_user_session_path, current_path
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class ActionDispatch::IntegrationTest
|
2
|
+
|
3
|
+
def warden
|
4
|
+
request.env['warden']
|
5
|
+
end
|
6
|
+
|
7
|
+
def create_full_user
|
8
|
+
@user ||= begin
|
9
|
+
user = User.create!(
|
10
|
+
:email => 'user@email.invalid',
|
11
|
+
:password => '12345678',
|
12
|
+
:password_confirmation => '12345678'
|
13
|
+
)
|
14
|
+
user
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def enable_otp_and_sign_in_with_otp
|
19
|
+
enable_otp_and_sign_in.tap do |user|
|
20
|
+
fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
|
21
|
+
click_button 'Submit Token'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def enable_otp_and_sign_in
|
27
|
+
user = create_full_user
|
28
|
+
sign_user_in(user)
|
29
|
+
visit user_otp_token_path
|
30
|
+
check 'user_otp_enabled'
|
31
|
+
click_button 'Continue...'
|
32
|
+
|
33
|
+
Capybara.reset_sessions!
|
34
|
+
|
35
|
+
sign_user_in(user)
|
36
|
+
user
|
37
|
+
end
|
38
|
+
|
39
|
+
def sign_user_in(user = nil)
|
40
|
+
user ||= create_full_user
|
41
|
+
resource_name = user.class.name.underscore
|
42
|
+
visit send("new_#{resource_name}_session_path")
|
43
|
+
fill_in "#{resource_name}_email", :with => user.email
|
44
|
+
fill_in "#{resource_name}_password", :with => user.password
|
45
|
+
click_button 'Sign in'
|
46
|
+
user
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class ActiveSupport::TestCase
|
2
|
+
|
3
|
+
#
|
4
|
+
# Helpers for creating new users
|
5
|
+
#
|
6
|
+
def unique_identity
|
7
|
+
@@unique_identity_count ||= 0
|
8
|
+
@@unique_identity_count += 1
|
9
|
+
"user-#{@@unique_identity_count}@mail.invalid"
|
10
|
+
end
|
11
|
+
|
12
|
+
def valid_attributes(attributes={})
|
13
|
+
{ :email => unique_identity,
|
14
|
+
:password => '12345678',
|
15
|
+
:password_confirmation => '12345678' }.update(attributes)
|
16
|
+
end
|
17
|
+
|
18
|
+
def new_user(attributes={})
|
19
|
+
User.new(valid_attributes(attributes)).save
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'model_tests_helper'
|
3
|
+
|
4
|
+
class OtpAuthenticatableTest < ActiveSupport::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
new_user
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'new users have a non-nil secret set' do
|
11
|
+
assert_not_nil User.first.otp_auth_secret
|
12
|
+
end
|
13
|
+
|
14
|
+
test 'new users have OTP disabled by default' do
|
15
|
+
assert !User.first.otp_enabled
|
16
|
+
end
|
17
|
+
|
18
|
+
test 'users should have an instance of TOTP/ROTP objects' do
|
19
|
+
u = User.first
|
20
|
+
assert u.time_based_otp.is_a? ROTP::TOTP
|
21
|
+
assert u.recovery_otp.is_a? ROTP::HOTP
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'users should have their otp_auth_secret/persistence_seed set on creation' do
|
25
|
+
assert User.first.otp_auth_secret
|
26
|
+
assert User.first.otp_persistence_seed
|
27
|
+
end
|
28
|
+
|
29
|
+
test 'reset_otp_credentials should generate new secrets and disable OTP' do
|
30
|
+
u = User.first
|
31
|
+
u.update_attribute(:otp_enabled, true)
|
32
|
+
assert u.otp_enabled
|
33
|
+
otp_auth_secret = u.otp_auth_secret
|
34
|
+
otp_persistence_seed = u.otp_persistence_seed
|
35
|
+
|
36
|
+
u.reset_otp_credentials!
|
37
|
+
assert !(otp_auth_secret == u.otp_auth_secret)
|
38
|
+
assert !(otp_persistence_seed == u.otp_persistence_seed)
|
39
|
+
assert !u.otp_enabled
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'reset_otp_persistence should generate new persistence_seed but NOT change the otp_auth_secret' do
|
43
|
+
u = User.first
|
44
|
+
u.update_attribute(:otp_enabled, true)
|
45
|
+
assert u.otp_enabled
|
46
|
+
otp_auth_secret = u.otp_auth_secret
|
47
|
+
otp_persistence_seed = u.otp_persistence_seed
|
48
|
+
|
49
|
+
u.reset_otp_persistence!
|
50
|
+
assert (otp_auth_secret == u.otp_auth_secret)
|
51
|
+
assert !(otp_persistence_seed == u.otp_persistence_seed)
|
52
|
+
assert u.otp_enabled
|
53
|
+
end
|
54
|
+
|
55
|
+
test 'generating a challenge, should retrieve the user later' do
|
56
|
+
u = User.first
|
57
|
+
u.update_attribute(:otp_enabled, true)
|
58
|
+
challenge = u.generate_otp_challenge!
|
59
|
+
|
60
|
+
w = User.find_valid_otp_challenge(challenge)
|
61
|
+
assert w.is_a? User
|
62
|
+
assert_equal w,u
|
63
|
+
end
|
64
|
+
|
65
|
+
test 'expiring the challenge, should retrieve nothing' do
|
66
|
+
u = User.first
|
67
|
+
u.update_attribute(:otp_enabled, true)
|
68
|
+
challenge = u.generate_otp_challenge!(1.second)
|
69
|
+
sleep(2)
|
70
|
+
|
71
|
+
w = User.find_valid_otp_challenge(challenge)
|
72
|
+
assert_nil w
|
73
|
+
end
|
74
|
+
|
75
|
+
test 'expired challenges should not be valid' do
|
76
|
+
u = User.first
|
77
|
+
u.update_attribute(:otp_enabled, true)
|
78
|
+
challenge = u.generate_otp_challenge!(1.second)
|
79
|
+
sleep(2)
|
80
|
+
assert_equal false, u.otp_challenge_valid?
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
test 'generated otp token should be valid for the user' do
|
85
|
+
u = User.first
|
86
|
+
u.update_attribute(:otp_enabled, true)
|
87
|
+
|
88
|
+
secret = u.otp_auth_secret
|
89
|
+
token = ROTP::TOTP.new(secret).now
|
90
|
+
|
91
|
+
assert_equal true, u.validate_otp_token(token)
|
92
|
+
end
|
93
|
+
|
94
|
+
test 'generated otp token, out of drift window, should be NOT valid for the user' do
|
95
|
+
u = User.first
|
96
|
+
u.update_attribute(:otp_enabled, true)
|
97
|
+
|
98
|
+
secret = u.otp_auth_secret
|
99
|
+
|
100
|
+
[3.minutes.from_now, 3.minutes.ago].each do |time|
|
101
|
+
token = ROTP::TOTP.new(secret).at(time)
|
102
|
+
assert_equal false, u.valid_otp_token?(token)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
test 'recovery secrets should be valid, and valid only once' do
|
107
|
+
u = User.first
|
108
|
+
u.update_attribute(:otp_enabled, true)
|
109
|
+
recovery = u.next_otp_recovery_tokens
|
110
|
+
|
111
|
+
assert u.valid_otp_recovery_token? recovery.fetch(0)
|
112
|
+
assert_equal false, u.valid_otp_recovery_token?(recovery.fetch(0))
|
113
|
+
assert u.valid_otp_recovery_token? recovery.fetch(2)
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|