devise-otp-rails5 0.2.4

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 (89) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +42 -0
  3. data/.travis.yml +12 -0
  4. data/Gemfile +25 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +140 -0
  7. data/Rakefile +42 -0
  8. data/app/assets/javascripts/devise-otp.js +1 -0
  9. data/app/assets/javascripts/qrcode.js +609 -0
  10. data/app/controllers/devise_otp/credentials_controller.rb +106 -0
  11. data/app/controllers/devise_otp/tokens_controller.rb +111 -0
  12. data/app/views/devise_otp/credentials/refresh.html.erb +20 -0
  13. data/app/views/devise_otp/credentials/show.html.erb +23 -0
  14. data/app/views/devise_otp/tokens/_token_secret.html.erb +19 -0
  15. data/app/views/devise_otp/tokens/_trusted_devices.html.erb +10 -0
  16. data/app/views/devise_otp/tokens/recovery.html.erb +21 -0
  17. data/app/views/devise_otp/tokens/recovery_codes.text.erb +3 -0
  18. data/app/views/devise_otp/tokens/show.html.erb +19 -0
  19. data/config/locales/en.yml +66 -0
  20. data/devise-otp.gemspec +25 -0
  21. data/lib/devise-otp.rb +83 -0
  22. data/lib/devise-otp/version.rb +5 -0
  23. data/lib/devise_otp_authenticatable/controllers/helpers.rb +168 -0
  24. data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +33 -0
  25. data/lib/devise_otp_authenticatable/engine.rb +23 -0
  26. data/lib/devise_otp_authenticatable/hooks.rb +13 -0
  27. data/lib/devise_otp_authenticatable/hooks/sessions.rb +59 -0
  28. data/lib/devise_otp_authenticatable/mapping.rb +19 -0
  29. data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +137 -0
  30. data/lib/devise_otp_authenticatable/routes.rb +32 -0
  31. data/lib/generators/active_record/devise_otp_generator.rb +13 -0
  32. data/lib/generators/active_record/templates/migration.rb +27 -0
  33. data/lib/generators/devise_otp/devise_otp_generator.rb +17 -0
  34. data/lib/generators/devise_otp/install_generator.rb +53 -0
  35. data/lib/generators/devise_otp/views_generator.rb +19 -0
  36. data/test/dummy/README.rdoc +261 -0
  37. data/test/dummy/Rakefile +7 -0
  38. data/test/dummy/app/assets/javascripts/application.js +13 -0
  39. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  40. data/test/dummy/app/controllers/application_controller.rb +4 -0
  41. data/test/dummy/app/controllers/posts_controller.rb +83 -0
  42. data/test/dummy/app/helpers/application_helper.rb +2 -0
  43. data/test/dummy/app/helpers/posts_helper.rb +2 -0
  44. data/test/dummy/app/mailers/.gitkeep +0 -0
  45. data/test/dummy/app/models/post.rb +2 -0
  46. data/test/dummy/app/models/user.rb +20 -0
  47. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  48. data/test/dummy/app/views/posts/_form.html.erb +25 -0
  49. data/test/dummy/app/views/posts/edit.html.erb +6 -0
  50. data/test/dummy/app/views/posts/index.html.erb +25 -0
  51. data/test/dummy/app/views/posts/new.html.erb +5 -0
  52. data/test/dummy/app/views/posts/show.html.erb +15 -0
  53. data/test/dummy/config.ru +4 -0
  54. data/test/dummy/config/application.rb +67 -0
  55. data/test/dummy/config/boot.rb +10 -0
  56. data/test/dummy/config/database.yml +25 -0
  57. data/test/dummy/config/environment.rb +5 -0
  58. data/test/dummy/config/environments/development.rb +30 -0
  59. data/test/dummy/config/environments/production.rb +69 -0
  60. data/test/dummy/config/environments/test.rb +36 -0
  61. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  62. data/test/dummy/config/initializers/devise.rb +253 -0
  63. data/test/dummy/config/initializers/inflections.rb +15 -0
  64. data/test/dummy/config/initializers/mime_types.rb +5 -0
  65. data/test/dummy/config/initializers/secret_token.rb +8 -0
  66. data/test/dummy/config/initializers/session_store.rb +8 -0
  67. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  68. data/test/dummy/config/locales/en.yml +5 -0
  69. data/test/dummy/config/routes.rb +6 -0
  70. data/test/dummy/db/migrate/20130125101430_create_users.rb +9 -0
  71. data/test/dummy/db/migrate/20130131092406_add_devise_to_users.rb +53 -0
  72. data/test/dummy/db/migrate/20130131142320_create_posts.rb +10 -0
  73. data/test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb +28 -0
  74. data/test/dummy/lib/assets/.gitkeep +0 -0
  75. data/test/dummy/public/404.html +26 -0
  76. data/test/dummy/public/422.html +26 -0
  77. data/test/dummy/public/500.html +25 -0
  78. data/test/dummy/public/favicon.ico +0 -0
  79. data/test/dummy/script/rails +6 -0
  80. data/test/integration/persistence_test.rb +65 -0
  81. data/test/integration/refresh_test.rb +106 -0
  82. data/test/integration/sign_in_test.rb +87 -0
  83. data/test/integration/token_test.rb +34 -0
  84. data/test/integration_tests_helper.rb +66 -0
  85. data/test/model_tests_helper.rb +22 -0
  86. data/test/models/otp_authenticatable_test.rb +122 -0
  87. data/test/orm/active_record.rb +4 -0
  88. data/test/test_helper.rb +22 -0
  89. metadata +253 -0
@@ -0,0 +1,28 @@
1
+ class DeviseOtpAddToUsers < ActiveRecord::Migration[5.0]
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,65 @@
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 'trusted status should expire' 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
+ click_link('Trust this browser')
56
+ assert_text 'Your browser is trusted.'
57
+ sign_out
58
+
59
+ sleep User.otp_trust_persistence.to_i + 1
60
+ sign_user_in
61
+
62
+ assert_equal user_otp_credential_path, current_path
63
+ end
64
+
65
+ end
@@ -0,0 +1,106 @@
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
+
93
+ test 'and rejected when the token is blank or null' do
94
+ user = enable_otp_and_sign_in_with_otp
95
+
96
+ sleep(2)
97
+ visit user_otp_token_path
98
+ assert_equal refresh_user_otp_credential_path, current_path
99
+
100
+ fill_in 'user_refresh_password', :with => '12345678'
101
+ fill_in 'user_token', :with => ''
102
+ click_button 'Continue...'
103
+
104
+ assert_equal refresh_user_otp_credential_path, current_path
105
+ end
106
+ end
@@ -0,0 +1,87 @@
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
+ page.has_content?('Log in') ? click_button('Log in') : 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 '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
+
63
+ test 'successful token authentication' do
64
+ user = enable_otp_and_sign_in
65
+
66
+ fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
67
+ click_button 'Submit Token'
68
+
69
+ assert_equal root_path, current_path
70
+ end
71
+
72
+
73
+ test 'should fail if the the challenge times out' do
74
+ old_timeout = User.otp_authentication_timeout
75
+ User.otp_authentication_timeout = 1.second
76
+
77
+ user = enable_otp_and_sign_in
78
+
79
+ sleep(2)
80
+
81
+ fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
82
+ click_button 'Submit Token'
83
+
84
+ User.otp_authentication_timeout = old_timeout
85
+ assert_equal new_user_session_path, current_path
86
+ end
87
+ end
@@ -0,0 +1,34 @@
1
+ require 'test_helper'
2
+ require 'integration_tests_helper'
3
+
4
+ class TokenTest < ActionDispatch::IntegrationTest
5
+
6
+
7
+ def teardown
8
+ Capybara.reset_sessions!
9
+ end
10
+
11
+ test 'disabling OTP after successfully enabling' do
12
+
13
+ # log in 1fa
14
+ user = enable_otp_and_sign_in
15
+ assert_equal user_otp_credential_path, current_path
16
+
17
+ # otp 2fa
18
+ fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
19
+ click_button 'Submit Token'
20
+ assert_equal root_path, current_path
21
+
22
+ # disable OTP
23
+ disable_otp
24
+
25
+ # logout
26
+ sign_out
27
+
28
+ # log back in 1fa
29
+ sign_user_in(user)
30
+
31
+ assert_equal root_path, current_path
32
+
33
+ end
34
+ end
@@ -0,0 +1,66 @@
1
+ class ActionDispatch::IntegrationTest
2
+ include Warden::Test::Helpers
3
+
4
+ def warden
5
+ request.env['warden']
6
+ end
7
+
8
+ def create_full_user
9
+ @user ||= begin
10
+ user = User.create!(
11
+ :email => 'user@email.invalid',
12
+ :password => '12345678',
13
+ :password_confirmation => '12345678'
14
+ )
15
+ user
16
+ end
17
+ end
18
+
19
+ def enable_otp_and_sign_in_with_otp
20
+ enable_otp_and_sign_in.tap do |user|
21
+ fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
22
+ click_button 'Submit Token'
23
+ end
24
+ end
25
+
26
+
27
+ def enable_otp_and_sign_in
28
+ user = create_full_user
29
+ sign_user_in(user)
30
+ visit user_otp_token_path
31
+ check 'user_otp_enabled'
32
+ click_button 'Continue...'
33
+
34
+ Capybara.reset_sessions!
35
+
36
+ sign_user_in(user)
37
+ user
38
+ end
39
+
40
+ def otp_challenge_for(user)
41
+ fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
42
+ click_button 'Submit Token'
43
+ end
44
+
45
+ def disable_otp
46
+ visit user_otp_token_path
47
+ uncheck 'user_otp_enabled'
48
+ click_button 'Continue...'
49
+ end
50
+
51
+ def sign_out
52
+ logout :user
53
+ end
54
+
55
+ def sign_user_in(user = nil)
56
+ user ||= create_full_user
57
+ resource_name = user.class.name.underscore
58
+ visit send("new_#{resource_name}_session_path")
59
+ fill_in "#{resource_name}_email", :with => user.email
60
+ fill_in "#{resource_name}_password", :with => user.password
61
+
62
+ page.has_content?('Log in') ? click_button('Log in') : click_button('Sign in')
63
+ user
64
+ end
65
+
66
+ end