devise-2fa 0.1.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.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +36 -0
  3. data/.hound.yml +2 -0
  4. data/.ruby-style.yml +1248 -0
  5. data/.travis.yml +28 -0
  6. data/Gemfile +25 -0
  7. data/LICENSE +21 -0
  8. data/README.md +130 -0
  9. data/Rakefile +41 -0
  10. data/app/controllers/devise/credentials_controller.rb +100 -0
  11. data/app/controllers/devise/tokens_controller.rb +99 -0
  12. data/app/views/devise/credentials/refresh.html.erb +20 -0
  13. data/app/views/devise/credentials/show.html.erb +23 -0
  14. data/app/views/devise/tokens/_token_secret.html.erb +19 -0
  15. data/app/views/devise/tokens/_trusted_devices.html.erb +10 -0
  16. data/app/views/devise/tokens/recovery.html.erb +21 -0
  17. data/app/views/devise/tokens/recovery_codes.text.erb +3 -0
  18. data/app/views/devise/tokens/show.html.erb +19 -0
  19. data/config/locales/en.yml +57 -0
  20. data/devise-2fa.gemspec +27 -0
  21. data/lib/devise-2fa.rb +74 -0
  22. data/lib/devise-2fa/version.rb +5 -0
  23. data/lib/devise_two_factorable/controllers/helpers.rb +136 -0
  24. data/lib/devise_two_factorable/controllers/url_helpers.rb +30 -0
  25. data/lib/devise_two_factorable/engine.rb +22 -0
  26. data/lib/devise_two_factorable/helpers.rb +136 -0
  27. data/lib/devise_two_factorable/hooks.rb +11 -0
  28. data/lib/devise_two_factorable/hooks/sessions.rb +49 -0
  29. data/lib/devise_two_factorable/mapping.rb +12 -0
  30. data/lib/devise_two_factorable/models/two_factorable.rb +131 -0
  31. data/lib/devise_two_factorable/routes.rb +26 -0
  32. data/lib/devise_two_factorable/two_factorable.rb +131 -0
  33. data/lib/generators/active_record/devise_two_factor_generator.rb +32 -0
  34. data/lib/generators/active_record/templates/migration.rb +27 -0
  35. data/lib/generators/devise_two_factor/devise_two_factor_generator.rb +16 -0
  36. data/lib/generators/devise_two_factor/install_generator.rb +52 -0
  37. data/lib/generators/devise_two_factor/views_generator.rb +19 -0
  38. data/lib/generators/mongoid/devise_two_factor_generator.rb +34 -0
  39. data/test/dummy/README.rdoc +261 -0
  40. data/test/dummy/Rakefile +7 -0
  41. data/test/dummy/app/assets/javascripts/application.js +13 -0
  42. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  43. data/test/dummy/app/controllers/application_controller.rb +4 -0
  44. data/test/dummy/app/controllers/posts_controller.rb +83 -0
  45. data/test/dummy/app/helpers/application_helper.rb +2 -0
  46. data/test/dummy/app/helpers/posts_helper.rb +2 -0
  47. data/test/dummy/app/mailers/.gitkeep +0 -0
  48. data/test/dummy/app/models/post.rb +2 -0
  49. data/test/dummy/app/models/user.rb +20 -0
  50. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  51. data/test/dummy/app/views/posts/_form.html.erb +25 -0
  52. data/test/dummy/app/views/posts/edit.html.erb +6 -0
  53. data/test/dummy/app/views/posts/index.html.erb +25 -0
  54. data/test/dummy/app/views/posts/new.html.erb +5 -0
  55. data/test/dummy/app/views/posts/show.html.erb +15 -0
  56. data/test/dummy/config.ru +4 -0
  57. data/test/dummy/config/application.rb +67 -0
  58. data/test/dummy/config/boot.rb +10 -0
  59. data/test/dummy/config/database.yml +25 -0
  60. data/test/dummy/config/environment.rb +5 -0
  61. data/test/dummy/config/environments/development.rb +37 -0
  62. data/test/dummy/config/environments/production.rb +73 -0
  63. data/test/dummy/config/environments/test.rb +36 -0
  64. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  65. data/test/dummy/config/initializers/devise.rb +251 -0
  66. data/test/dummy/config/initializers/inflections.rb +15 -0
  67. data/test/dummy/config/initializers/mime_types.rb +5 -0
  68. data/test/dummy/config/initializers/secret_token.rb +8 -0
  69. data/test/dummy/config/initializers/session_store.rb +8 -0
  70. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  71. data/test/dummy/config/locales/en.yml +5 -0
  72. data/test/dummy/config/routes.rb +6 -0
  73. data/test/dummy/db/migrate/20130125101430_create_users.rb +9 -0
  74. data/test/dummy/db/migrate/20130131092406_add_devise_to_users.rb +52 -0
  75. data/test/dummy/db/migrate/20130131142320_create_posts.rb +10 -0
  76. data/test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb +28 -0
  77. data/test/dummy/lib/assets/.gitkeep +0 -0
  78. data/test/dummy/public/404.html +26 -0
  79. data/test/dummy/public/422.html +26 -0
  80. data/test/dummy/public/500.html +25 -0
  81. data/test/dummy/public/favicon.ico +0 -0
  82. data/test/dummy/script/rails +6 -0
  83. data/test/integration/persistence_test.rb +63 -0
  84. data/test/integration/refresh_test.rb +103 -0
  85. data/test/integration/sign_in_test.rb +85 -0
  86. data/test/integration/token_test.rb +30 -0
  87. data/test/integration_tests_helper.rb +64 -0
  88. data/test/model_tests_helper.rb +20 -0
  89. data/test/models/two_factorable_test.rb +120 -0
  90. data/test/orm/active_record.rb +4 -0
  91. data/test/orm/mongoid.rb +13 -0
  92. data/test/support/mongoid.yml +6 -0
  93. data/test/support/symmetric_encryption.yml +70 -0
  94. data/test/test_helper.rb +18 -0
  95. metadata +269 -0
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,63 @@
1
+ require 'test_helper'
2
+ require 'integration_tests_helper'
3
+
4
+ class PersistenceTest < ActionDispatch::IntegrationTest
5
+ def setup
6
+ @old_persistence = User.otp_trust_persistence
7
+ User.otp_trust_persistence = 3.seconds
8
+ end
9
+
10
+ def teardown
11
+ User.otp_trust_persistence = @old_persistence
12
+ Capybara.reset_sessions!
13
+ end
14
+
15
+ test 'a user should be requested the otp challenge every log in' do
16
+ # log in 1fa
17
+ user = enable_otp_and_sign_in
18
+ otp_challenge_for user
19
+
20
+ visit user_token_path
21
+ assert_equal user_token_path, current_path
22
+
23
+ sign_out
24
+ sign_user_in
25
+
26
+ assert_equal user_credential_path, current_path
27
+ end
28
+
29
+ test 'a user should be able to set their browser as trusted' do
30
+ # log in 1fa
31
+ user = enable_otp_and_sign_in
32
+ otp_challenge_for user
33
+
34
+ visit user_token_path
35
+ assert_equal user_token_path, current_path
36
+
37
+ click_link('Trust this browser')
38
+ assert_text 'Your browser is trusted.'
39
+ sign_out
40
+
41
+ sign_user_in
42
+
43
+ assert_equal root_path, current_path
44
+ end
45
+
46
+ test 'trusted status should expire' do
47
+ # log in 1fa
48
+ user = enable_otp_and_sign_in
49
+ otp_challenge_for user
50
+
51
+ visit user_token_path
52
+ assert_equal user_token_path, current_path
53
+
54
+ click_link('Trust this browser')
55
+ assert_text 'Your browser is trusted.'
56
+ sign_out
57
+
58
+ sleep User.otp_trust_persistence.to_i + 1
59
+ sign_user_in
60
+
61
+ assert_equal user_credential_path, current_path
62
+ end
63
+ end
@@ -0,0 +1,103 @@
1
+ require 'test_helper'
2
+ require 'integration_tests_helper'
3
+
4
+ class RefreshTest < ActionDispatch::IntegrationTest
5
+ def setup
6
+ @old_refresh = User.otp_credentials_refresh
7
+ User.otp_credentials_refresh = 1.second
8
+ end
9
+
10
+ def teardown
11
+ User.otp_credentials_refresh = @old_refresh
12
+ Capybara.reset_sessions!
13
+ end
14
+
15
+ test 'a user that just signed in should be able to access their OTP settings without refreshing' do
16
+ sign_user_in
17
+
18
+ visit user_token_path
19
+ assert_equal user_token_path, current_path
20
+ end
21
+
22
+ test 'a user should be prompted for credentials when the credentials_refresh time is expired' do
23
+ sign_user_in
24
+ visit user_token_path
25
+ assert_equal user_token_path, current_path
26
+
27
+ sleep(2)
28
+
29
+ visit user_token_path
30
+ assert_equal refresh_user_credential_path, current_path
31
+ end
32
+
33
+ test 'a user should be able to access their OTP settings after refreshing' do
34
+ sign_user_in
35
+ visit user_token_path
36
+ assert_equal user_token_path, current_path
37
+
38
+ sleep(2)
39
+
40
+ visit user_token_path
41
+ assert_equal refresh_user_credential_path, current_path
42
+
43
+ fill_in 'user_refresh_password', with: '12345678'
44
+ click_button 'Continue...'
45
+ assert_equal user_token_path, current_path
46
+ end
47
+
48
+ test 'a user should NOT be able to access their OTP settings unless refreshing' do
49
+ sign_user_in
50
+ visit user_token_path
51
+ assert_equal user_token_path, current_path
52
+
53
+ sleep(2)
54
+
55
+ visit user_token_path
56
+ assert_equal refresh_user_credential_path, current_path
57
+
58
+ fill_in 'user_refresh_password', with: '12345670'
59
+ click_button 'Continue...'
60
+ assert_equal refresh_user_credential_path, current_path
61
+ end
62
+
63
+ test 'user should be asked their OTP challenge in order to refresh, if they have OTP' do
64
+ enable_otp_and_sign_in_with_otp
65
+
66
+ sleep(2)
67
+ visit user_token_path
68
+ assert_equal refresh_user_credential_path, current_path
69
+
70
+ fill_in 'user_refresh_password', with: '12345678'
71
+ click_button 'Continue...'
72
+
73
+ assert_equal refresh_user_credential_path, current_path
74
+ end
75
+
76
+ test 'user should be finally be able to access their settings, if they provide both a password and a valid OTP token' do
77
+ user = enable_otp_and_sign_in_with_otp
78
+
79
+ sleep(2)
80
+ visit user_token_path
81
+ assert_equal refresh_user_credential_path, current_path
82
+
83
+ fill_in 'user_refresh_password', with: '12345678'
84
+ fill_in 'user_token', with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
85
+ click_button 'Continue...'
86
+
87
+ assert_equal user_token_path, current_path
88
+ end
89
+
90
+ test 'and rejected when the token is blank or null' do
91
+ user = enable_otp_and_sign_in_with_otp
92
+
93
+ sleep(2)
94
+ visit user_token_path
95
+ assert_equal refresh_user_credential_path, current_path
96
+
97
+ fill_in 'user_refresh_password', with: '12345678'
98
+ fill_in 'user_token', with: ''
99
+ click_button 'Continue...'
100
+
101
+ assert_equal refresh_user_credential_path, current_path
102
+ end
103
+ end
@@ -0,0 +1,85 @@
1
+ require 'test_helper'
2
+ require 'integration_tests_helper'
3
+
4
+ class SignInTest < ActionDispatch::IntegrationTest
5
+ def teardown
6
+ Capybara.reset_sessions!
7
+ end
8
+
9
+ test 'a new user should be able to sign in without using their token' do
10
+ create_full_user
11
+
12
+ visit new_user_session_path
13
+ fill_in 'user_email', with: 'user@email.invalid'
14
+ fill_in 'user_password', with: '12345678'
15
+ page.has_content?('Log in') ? click_button('Log in') : click_button('Sign in')
16
+
17
+ assert_equal root_path, current_path
18
+ end
19
+
20
+ test 'a new user, just signed in, should be able to sign in and enable their OTP authentication' do
21
+ user = sign_user_in
22
+
23
+ visit user_token_path
24
+ assert !page.has_content?('Your token secret')
25
+
26
+ check 'user_otp_enabled'
27
+ click_button 'Continue...'
28
+
29
+ assert_equal user_token_path, current_path
30
+
31
+ assert page.has_content?('Your token secret')
32
+ assert !user.otp_auth_secret.nil?
33
+ assert !user.otp_persistence_seed.nil?
34
+ end
35
+
36
+ test 'a new user should be able to sign in enable OTP and be prompted for their token' do
37
+ enable_otp_and_sign_in
38
+
39
+ assert_equal user_credential_path, current_path
40
+ end
41
+
42
+ test 'fail token authentication' do
43
+ enable_otp_and_sign_in
44
+ assert_equal user_credential_path, current_path
45
+
46
+ fill_in 'user_token', with: '123456'
47
+ click_button 'Submit Token'
48
+
49
+ assert_equal new_user_session_path, current_path
50
+ end
51
+
52
+ test 'fail blank token authentication' do
53
+ enable_otp_and_sign_in
54
+ assert_equal user_credential_path, current_path
55
+
56
+ fill_in 'user_token', with: ''
57
+ click_button 'Submit Token'
58
+
59
+ assert_equal user_credential_path, current_path
60
+ end
61
+
62
+ test 'successful token authentication' do
63
+ user = enable_otp_and_sign_in
64
+
65
+ fill_in 'user_token', with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
66
+ click_button 'Submit Token'
67
+
68
+ assert_equal root_path, current_path
69
+ end
70
+
71
+ test 'should fail if the the challenge times out' do
72
+ old_timeout = User.otp_authentication_timeout
73
+ User.otp_authentication_timeout = 1.second
74
+
75
+ user = enable_otp_and_sign_in
76
+
77
+ sleep(2)
78
+
79
+ fill_in 'user_token', with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
80
+ click_button 'Submit Token'
81
+
82
+ User.otp_authentication_timeout = old_timeout
83
+ assert_equal new_user_session_path, current_path
84
+ end
85
+ end
@@ -0,0 +1,30 @@
1
+ require 'test_helper'
2
+ require 'integration_tests_helper'
3
+
4
+ class TokenTest < ActionDispatch::IntegrationTest
5
+ def teardown
6
+ Capybara.reset_sessions!
7
+ end
8
+
9
+ test 'disabling OTP after successfully enabling' do
10
+ # log in 1fa
11
+ user = enable_otp_and_sign_in
12
+ assert_equal user_credential_path, current_path
13
+
14
+ # otp two_factor
15
+ fill_in 'user_token', with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
16
+ click_button 'Submit Token'
17
+ assert_equal root_path, current_path
18
+
19
+ # disable OTP
20
+ disable_otp
21
+
22
+ # logout
23
+ sign_out
24
+
25
+ # log back in 1fa
26
+ sign_user_in(user)
27
+
28
+ assert_equal root_path, current_path
29
+ end
30
+ end
@@ -0,0 +1,64 @@
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
+ def enable_otp_and_sign_in
27
+ user = create_full_user
28
+ sign_user_in(user)
29
+ visit user_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 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
+
44
+ def disable_otp
45
+ visit user_token_path
46
+ uncheck 'user_otp_enabled'
47
+ click_button 'Continue...'
48
+ end
49
+
50
+ def sign_out
51
+ logout :user
52
+ end
53
+
54
+ def sign_user_in(user = nil)
55
+ user ||= create_full_user
56
+ resource_name = user.class.name.underscore
57
+ visit send("new_#{resource_name}_session_path")
58
+ fill_in "#{resource_name}_email", with: user.email
59
+ fill_in "#{resource_name}_password", with: user.password
60
+
61
+ page.has_content?('Log in') ? click_button('Log in') : click_button('Sign in')
62
+ user
63
+ end
64
+ end
@@ -0,0 +1,20 @@
1
+ class ActiveSupport::TestCase
2
+ #
3
+ # Helpers for creating new users
4
+ #
5
+ def unique_identity
6
+ @@unique_identity_count ||= 0
7
+ @@unique_identity_count += 1
8
+ "user-#{@@unique_identity_count}@mail.invalid"
9
+ end
10
+
11
+ def valid_attributes(attributes = {})
12
+ { email: unique_identity,
13
+ password: '12345678',
14
+ password_confirmation: '12345678' }.update(attributes)
15
+ end
16
+
17
+ def new_user(attributes = {})
18
+ User.new(valid_attributes(attributes)).save
19
+ end
20
+ end