devise-otp 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +36 -0
  3. data/.gitignore +0 -0
  4. data/Gemfile +1 -22
  5. data/LICENSE.txt +0 -0
  6. data/README.md +43 -66
  7. data/Rakefile +0 -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/devise/credentials_controller.rb +102 -0
  11. data/app/controllers/devise_otp/devise/tokens_controller.rb +112 -0
  12. data/app/views/devise/credentials/refresh.html.erb +19 -0
  13. data/app/views/devise/credentials/show.html.erb +31 -0
  14. data/app/views/devise/tokens/_token_secret.html.erb +23 -0
  15. data/app/views/devise/tokens/_trusted_devices.html.erb +12 -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 +21 -0
  19. data/config/locales/en.yml +10 -10
  20. data/devise-otp.gemspec +14 -9
  21. data/docs/QR_CODES.md +48 -0
  22. data/lib/devise-otp/version.rb +1 -1
  23. data/lib/devise-otp.rb +22 -14
  24. data/lib/devise_otp_authenticatable/controllers/helpers.rb +29 -16
  25. data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +6 -9
  26. data/lib/devise_otp_authenticatable/engine.rb +22 -13
  27. data/lib/devise_otp_authenticatable/hooks/sessions.rb +8 -7
  28. data/lib/devise_otp_authenticatable/hooks.rb +1 -1
  29. data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +28 -28
  30. data/lib/devise_otp_authenticatable/routes.rb +9 -10
  31. data/lib/generators/active_record/devise_otp_generator.rb +1 -1
  32. data/lib/generators/active_record/templates/migration.rb +1 -2
  33. data/lib/generators/devise_otp/devise_otp_generator.rb +0 -0
  34. data/lib/generators/devise_otp/install_generator.rb +30 -5
  35. data/lib/generators/devise_otp/views_generator.rb +2 -3
  36. data/test/dummy/README.rdoc +0 -0
  37. data/test/dummy/Rakefile +0 -0
  38. data/test/dummy/app/assets/config/manifest.js +2 -0
  39. data/test/dummy/app/assets/javascripts/application.js +1 -0
  40. data/test/dummy/app/assets/stylesheets/application.css +0 -0
  41. data/test/dummy/app/controllers/application_controller.rb +1 -1
  42. data/test/dummy/app/controllers/posts_controller.rb +2 -0
  43. data/test/dummy/app/helpers/application_helper.rb +0 -0
  44. data/test/dummy/app/helpers/posts_helper.rb +0 -0
  45. data/test/dummy/app/mailers/.gitkeep +0 -0
  46. data/test/dummy/app/models/post.rb +0 -0
  47. data/test/dummy/app/models/user.rb +1 -1
  48. data/test/dummy/app/views/layouts/application.html.erb +0 -0
  49. data/test/dummy/app/views/posts/_form.html.erb +0 -0
  50. data/test/dummy/app/views/posts/edit.html.erb +0 -0
  51. data/test/dummy/app/views/posts/index.html.erb +0 -0
  52. data/test/dummy/app/views/posts/new.html.erb +0 -0
  53. data/test/dummy/app/views/posts/show.html.erb +0 -0
  54. data/test/dummy/config/application.rb +2 -1
  55. data/test/dummy/config/boot.rb +0 -0
  56. data/test/dummy/config/database.yml +1 -1
  57. data/test/dummy/config/environment.rb +0 -0
  58. data/test/dummy/config/environments/development.rb +0 -7
  59. data/test/dummy/config/environments/production.rb +0 -4
  60. data/test/dummy/config/environments/test.rb +0 -0
  61. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -0
  62. data/test/dummy/config/initializers/devise.rb +0 -0
  63. data/test/dummy/config/initializers/inflections.rb +0 -0
  64. data/test/dummy/config/initializers/mime_types.rb +0 -0
  65. data/test/dummy/config/initializers/secret_token.rb +0 -0
  66. data/test/dummy/config/initializers/session_store.rb +0 -0
  67. data/test/dummy/config/initializers/wrap_parameters.rb +0 -0
  68. data/test/dummy/config/locales/en.yml +0 -0
  69. data/test/dummy/config/routes.rb +0 -0
  70. data/test/dummy/config.ru +0 -0
  71. data/test/dummy/db/migrate/20130125101430_create_users.rb +1 -1
  72. data/test/dummy/db/migrate/20130131092406_add_devise_to_users.rb +1 -1
  73. data/test/dummy/db/migrate/20130131142320_create_posts.rb +1 -1
  74. data/test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb +2 -2
  75. data/test/dummy/db/test.sqlite3-journal +0 -0
  76. data/test/dummy/lib/assets/.gitkeep +0 -0
  77. data/test/dummy/public/404.html +0 -0
  78. data/test/dummy/public/422.html +0 -0
  79. data/test/dummy/public/500.html +0 -0
  80. data/test/dummy/public/favicon.ico +0 -0
  81. data/test/integration/persistence_test.rb +81 -0
  82. data/test/integration/refresh_test.rb +2 -18
  83. data/test/integration/sign_in_test.rb +14 -4
  84. data/test/integration/token_test.rb +31 -0
  85. data/test/integration_tests_helper.rb +19 -2
  86. data/test/model_tests_helper.rb +0 -0
  87. data/test/models/otp_authenticatable_test.rb +14 -9
  88. data/test/orm/active_record.rb +3 -1
  89. data/test/test_helper.rb +71 -2
  90. metadata +135 -24
  91. data/.travis.yml +0 -11
  92. data/app/controllers/devise_otp/credentials_controller.rb +0 -106
  93. data/app/controllers/devise_otp/tokens_controller.rb +0 -105
  94. data/app/views/devise_otp/credentials/refresh.html.erb +0 -20
  95. data/app/views/devise_otp/credentials/show.html.erb +0 -23
  96. data/app/views/devise_otp/tokens/_token_secret.html.erb +0 -17
  97. data/app/views/devise_otp/tokens/recovery.html.erb +0 -21
  98. data/app/views/devise_otp/tokens/show.html.erb +0 -31
  99. data/lib/devise_otp_authenticatable/mapping.rb +0 -19
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
data/test/dummy/config.ru CHANGED
File without changes
@@ -1,4 +1,4 @@
1
- class CreateUsers < ActiveRecord::Migration
1
+ class CreateUsers < ActiveRecord::Migration[5.0]
2
2
  def change
3
3
  create_table :users do |t|
4
4
  t.string :name
@@ -1,4 +1,4 @@
1
- class AddDeviseToUsers < ActiveRecord::Migration
1
+ class AddDeviseToUsers < ActiveRecord::Migration[5.0]
2
2
  def self.up
3
3
  change_table(:users) do |t|
4
4
  ## Database authenticatable
@@ -1,4 +1,4 @@
1
- class CreatePosts < ActiveRecord::Migration
1
+ class CreatePosts < ActiveRecord::Migration[5.0]
2
2
  def change
3
3
  create_table :posts do |t|
4
4
  t.string :title
@@ -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,
Binary file
File without changes
File without changes
File without changes
File without changes
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 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
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 new_user_session_path
13
+ visit posts_path
14
14
  fill_in 'user_email', :with => 'user@email.invalid'
15
15
  fill_in 'user_password', :with => '12345678'
16
- click_button 'Sign in'
16
+ page.has_content?('Log in') ? click_button('Log in') : click_button('Sign in')
17
17
 
18
- assert_equal root_path, current_path
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
@@ -0,0 +1,31 @@
1
+ require 'test_helper'
2
+ require 'integration_tests_helper'
3
+
4
+ class TokenTest < ActionDispatch::IntegrationTest
5
+
6
+ def teardown
7
+ Capybara.reset_sessions!
8
+ end
9
+
10
+ test 'disabling OTP after successfully enabling' do
11
+ # log in 1fa
12
+ user = enable_otp_and_sign_in
13
+ assert_equal user_otp_credential_path, current_path
14
+
15
+ # otp 2fa
16
+ fill_in 'user_token', :with => ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
17
+ click_button 'Submit Token'
18
+ assert_equal root_path, current_path
19
+
20
+ # disable OTP
21
+ disable_otp
22
+
23
+ # logout
24
+ sign_out
25
+
26
+ # log back in 1fa
27
+ sign_user_in(user)
28
+
29
+ assert_equal root_path, current_path
30
+ 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,13 +36,30 @@ 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
+
44
+ def disable_otp
45
+ visit user_otp_token_path
46
+ uncheck 'user_otp_enabled'
47
+ click_button 'Continue...'
48
+ end
49
+
50
+ def sign_out
51
+ logout :user
52
+ end
53
+
39
54
  def sign_user_in(user = nil)
40
55
  user ||= create_full_user
41
56
  resource_name = user.class.name.underscore
42
57
  visit send("new_#{resource_name}_session_path")
43
58
  fill_in "#{resource_name}_email", :with => user.email
44
59
  fill_in "#{resource_name}_password", :with => user.password
45
- click_button 'Sign in'
60
+
61
+ page.has_content?('Log in') ? click_button('Log in') : click_button('Sign in')
46
62
  user
47
63
  end
64
+
48
65
  end
File without changes
@@ -3,17 +3,17 @@ require 'model_tests_helper'
3
3
 
4
4
  class OtpAuthenticatableTest < ActiveSupport::TestCase
5
5
 
6
- def setup
7
- new_user
6
+ def setup
7
+ new_user
8
8
  end
9
9
 
10
10
  test 'new users have a non-nil secret set' do
11
- assert_not_nil User.first.otp_auth_secret
12
- end
11
+ assert_not_nil User.first.otp_auth_secret
12
+ end
13
13
 
14
14
  test 'new users have OTP disabled by default' do
15
- assert !User.first.otp_enabled
16
- end
15
+ assert !User.first.otp_enabled
16
+ end
17
17
 
18
18
  test 'users should have an instance of TOTP/ROTP objects' do
19
19
  u = User.first
@@ -80,6 +80,12 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
80
80
  assert_equal false, u.otp_challenge_valid?
81
81
  end
82
82
 
83
+ test 'null otp challenge' do
84
+ u = User.first
85
+ u.update_attribute(:otp_enabled, true)
86
+ assert_equal false, u.validate_otp_token('')
87
+ assert_equal false, u.validate_otp_token(nil)
88
+ end
83
89
 
84
90
  test 'generated otp token should be valid for the user' do
85
91
  u = User.first
@@ -109,8 +115,7 @@ class OtpAuthenticatableTest < ActiveSupport::TestCase
109
115
  recovery = u.next_otp_recovery_tokens
110
116
 
111
117
  assert u.valid_otp_recovery_token? recovery.fetch(0)
112
- assert_equal false, u.valid_otp_recovery_token?(recovery.fetch(0))
118
+ assert_nil u.valid_otp_recovery_token?(recovery.fetch(0))
113
119
  assert u.valid_otp_recovery_token? recovery.fetch(2)
114
120
  end
115
-
116
- end
121
+ end
@@ -1,4 +1,6 @@
1
1
  ActiveRecord::Migration.verbose = false
2
2
  ActiveRecord::Base.logger = Logger.new(nil)
3
3
 
4
- ActiveRecord::Migrator.migrate(File.expand_path("../../dummy/db/migrate/", __FILE__))
4
+ migrations_path = File.expand_path("../../dummy/db/migrate/", __FILE__)
5
+
6
+ ActiveRecord::MigrationContext.new(migrations_path, ActiveRecord::SchemaMigration).migrate
data/test/test_helper.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  ENV["RAILS_ENV"] = "test"
2
2
  DEVISE_ORM = (ENV["DEVISE_ORM"] || :active_record).to_sym
3
3
 
4
- $:.unshift File.dirname(__FILE__)
5
4
  puts "\n==> Devise.orm = #{DEVISE_ORM.inspect}"
6
5
  require "dummy/config/environment"
7
6
  require "orm/#{DEVISE_ORM}"
8
7
  require 'rails/test_help'
9
8
  require 'capybara/rails'
9
+ require 'capybara/cuprite'
10
10
  require 'minitest/reporters'
11
11
 
12
12
  MiniTest::Reporters.use!
@@ -15,8 +15,77 @@ MiniTest::Reporters.use!
15
15
 
16
16
  #ActiveSupport::Deprecation.silenced = true
17
17
 
18
- #Capybara.default_driver = :selenium
18
+ # Use a module to not pollute the global namespace
19
+ module CapybaraHelper
20
+ def self.register_driver(driver_name, args = [])
21
+ opts = { headless: true, js_errors: true, window_size: [1920, 1200], browser_options: {} }
22
+ args.each do |arg|
23
+ opts[:browser_options][arg] = nil
24
+ end
25
+
26
+ Capybara.register_driver(driver_name) do |app|
27
+ Capybara::Cuprite::Driver.new(app, opts)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Register our own custom drivers
33
+ CapybaraHelper.register_driver(:headless_chrome, %w[disable-gpu no-sandbox disable-dev-shm-usage])
34
+
35
+ # Configure Capybara JS driver
36
+ Capybara.current_driver = :headless_chrome
37
+ Capybara.javascript_driver = :headless_chrome
38
+
39
+ # Configure Capybara server
40
+ Capybara.run_server = true
41
+ Capybara.server = :puma, { Silent: true }
19
42
 
20
43
  class ActionDispatch::IntegrationTest
21
44
  include Capybara::DSL
45
+
46
+ # What capybara calls a "page" in its DSL is actually a Capybara::Session
47
+ # and doesn't know about the *command* method that allows us to play with
48
+ # the Chrome API.
49
+ # See: https://rubydoc.info/github/jnicklas/capybara/master/Capybara/Session
50
+ #
51
+ # To enable downloads we need to do it on the browser's page object, so fetch it
52
+ # from this long method chain.
53
+ # See: https://github.com/rubycdp/ferrum/blob/master/lib/ferrum/page.rb
54
+ def enable_chrome_headless_downloads(session, directory)
55
+ page = session.driver.browser.page
56
+ page.command('Page.setDownloadBehavior', behavior: 'allow', downloadPath: directory)
57
+ end
58
+ end
59
+
60
+ # From https://collectiveidea.com/blog/archives/2012/01/27/testing-file-downloads-with-capybara-and-chromedriver
61
+ module DownloadHelper
62
+ extend self
63
+
64
+ TIMEOUT = 10
65
+
66
+ def downloads
67
+ Dir["/tmp/devise-otp/*"]
68
+ end
69
+
70
+ def wait_for_download(count: 1)
71
+ yield if block_given?
72
+
73
+ Timeout.timeout(TIMEOUT) do
74
+ sleep 0.2 until downloaded?(count)
75
+ end
76
+ end
77
+
78
+ def downloaded?(count)
79
+ !downloading? && downloads.size == count
80
+ end
81
+
82
+ def downloading?
83
+ downloads.grep(/\.crdownload$/).any?
84
+ end
85
+
86
+ def clear_downloads
87
+ FileUtils.rm_f(downloads)
88
+ end
22
89
  end
90
+
91
+ require "devise-otp"