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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +42 -0
  3. data/.travis.yml +11 -0
  4. data/Gemfile +25 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +124 -0
  7. data/Rakefile +42 -0
  8. data/app/controllers/devise_otp/credentials_controller.rb +106 -0
  9. data/app/controllers/devise_otp/tokens_controller.rb +105 -0
  10. data/app/views/devise_otp/credentials/refresh.html.erb +20 -0
  11. data/app/views/devise_otp/credentials/show.html.erb +23 -0
  12. data/app/views/devise_otp/tokens/_token_secret.html.erb +17 -0
  13. data/app/views/devise_otp/tokens/recovery.html.erb +21 -0
  14. data/app/views/devise_otp/tokens/show.html.erb +31 -0
  15. data/config/locales/en.yml +66 -0
  16. data/devise-otp.gemspec +25 -0
  17. data/lib/devise-otp.rb +76 -0
  18. data/lib/devise-otp/version.rb +5 -0
  19. data/lib/devise_otp_authenticatable/controllers/helpers.rb +144 -0
  20. data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +35 -0
  21. data/lib/devise_otp_authenticatable/engine.rb +23 -0
  22. data/lib/devise_otp_authenticatable/hooks.rb +13 -0
  23. data/lib/devise_otp_authenticatable/hooks/sessions.rb +57 -0
  24. data/lib/devise_otp_authenticatable/mapping.rb +19 -0
  25. data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +140 -0
  26. data/lib/devise_otp_authenticatable/routes.rb +30 -0
  27. data/lib/generators/active_record/devise_otp_generator.rb +13 -0
  28. data/lib/generators/active_record/templates/migration.rb +28 -0
  29. data/lib/generators/devise_otp/devise_otp_generator.rb +17 -0
  30. data/lib/generators/devise_otp/install_generator.rb +31 -0
  31. data/lib/generators/devise_otp/views_generator.rb +19 -0
  32. data/test/dummy/README.rdoc +261 -0
  33. data/test/dummy/Rakefile +7 -0
  34. data/test/dummy/app/assets/javascripts/application.js +13 -0
  35. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  36. data/test/dummy/app/controllers/application_controller.rb +4 -0
  37. data/test/dummy/app/controllers/posts_controller.rb +83 -0
  38. data/test/dummy/app/helpers/application_helper.rb +2 -0
  39. data/test/dummy/app/helpers/posts_helper.rb +2 -0
  40. data/test/dummy/app/mailers/.gitkeep +0 -0
  41. data/test/dummy/app/models/post.rb +2 -0
  42. data/test/dummy/app/models/user.rb +20 -0
  43. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  44. data/test/dummy/app/views/posts/_form.html.erb +25 -0
  45. data/test/dummy/app/views/posts/edit.html.erb +6 -0
  46. data/test/dummy/app/views/posts/index.html.erb +25 -0
  47. data/test/dummy/app/views/posts/new.html.erb +5 -0
  48. data/test/dummy/app/views/posts/show.html.erb +15 -0
  49. data/test/dummy/config.ru +4 -0
  50. data/test/dummy/config/application.rb +68 -0
  51. data/test/dummy/config/boot.rb +10 -0
  52. data/test/dummy/config/database.yml +25 -0
  53. data/test/dummy/config/environment.rb +5 -0
  54. data/test/dummy/config/environments/development.rb +37 -0
  55. data/test/dummy/config/environments/production.rb +73 -0
  56. data/test/dummy/config/environments/test.rb +36 -0
  57. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  58. data/test/dummy/config/initializers/devise.rb +253 -0
  59. data/test/dummy/config/initializers/inflections.rb +15 -0
  60. data/test/dummy/config/initializers/mime_types.rb +5 -0
  61. data/test/dummy/config/initializers/secret_token.rb +8 -0
  62. data/test/dummy/config/initializers/session_store.rb +8 -0
  63. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  64. data/test/dummy/config/locales/en.yml +5 -0
  65. data/test/dummy/config/routes.rb +6 -0
  66. data/test/dummy/db/migrate/20130125101430_create_users.rb +9 -0
  67. data/test/dummy/db/migrate/20130131092406_add_devise_to_users.rb +53 -0
  68. data/test/dummy/db/migrate/20130131142320_create_posts.rb +10 -0
  69. data/test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb +28 -0
  70. data/test/dummy/lib/assets/.gitkeep +0 -0
  71. data/test/dummy/public/404.html +26 -0
  72. data/test/dummy/public/422.html +26 -0
  73. data/test/dummy/public/500.html +25 -0
  74. data/test/dummy/public/favicon.ico +0 -0
  75. data/test/dummy/script/rails +6 -0
  76. data/test/integration/refresh_test.rb +92 -0
  77. data/test/integration/sign_in_test.rb +77 -0
  78. data/test/integration_tests_helper.rb +48 -0
  79. data/test/model_tests_helper.rb +22 -0
  80. data/test/models/otp_authenticatable_test.rb +116 -0
  81. data/test/orm/active_record.rb +4 -0
  82. data/test/test_helper.rb +19 -0
  83. 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