rodauth 0.10.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +146 -0
  3. data/README.rdoc +644 -220
  4. data/Rakefile +99 -11
  5. data/doc/account_expiration.rdoc +55 -0
  6. data/doc/base.rdoc +104 -0
  7. data/doc/change_login.rdoc +29 -0
  8. data/doc/change_password.rdoc +26 -0
  9. data/doc/close_account.rdoc +31 -0
  10. data/doc/confirm_password.rdoc +22 -0
  11. data/doc/create_account.rdoc +34 -0
  12. data/doc/disallow_password_reuse.rdoc +37 -0
  13. data/doc/email_base.rdoc +19 -0
  14. data/doc/jwt.rdoc +35 -0
  15. data/doc/lockout.rdoc +83 -0
  16. data/doc/login.rdoc +27 -0
  17. data/doc/login_password_requirements_base.rdoc +50 -0
  18. data/doc/logout.rdoc +21 -0
  19. data/doc/otp.rdoc +100 -0
  20. data/doc/password_complexity.rdoc +50 -0
  21. data/doc/password_expiration.rdoc +52 -0
  22. data/doc/password_grace_period.rdoc +10 -0
  23. data/doc/recovery_codes.rdoc +60 -0
  24. data/doc/release_notes/1.0.0.txt +443 -0
  25. data/doc/remember.rdoc +82 -0
  26. data/doc/reset_password.rdoc +70 -0
  27. data/doc/session_expiration.rdoc +27 -0
  28. data/doc/single_session.rdoc +43 -0
  29. data/doc/sms_codes.rdoc +119 -0
  30. data/doc/two_factor_base.rdoc +27 -0
  31. data/doc/verify_account.rdoc +70 -0
  32. data/doc/verify_account_grace_period.rdoc +15 -0
  33. data/doc/verify_change_login.rdoc +9 -0
  34. data/lib/roda/plugins/rodauth.rb +3 -262
  35. data/lib/rodauth.rb +260 -0
  36. data/lib/rodauth/features/account_expiration.rb +108 -0
  37. data/lib/rodauth/features/base.rb +479 -0
  38. data/lib/rodauth/features/change_login.rb +77 -0
  39. data/lib/rodauth/features/change_password.rb +66 -0
  40. data/lib/rodauth/features/close_account.rb +82 -0
  41. data/lib/rodauth/features/confirm_password.rb +51 -0
  42. data/lib/rodauth/features/create_account.rb +128 -0
  43. data/lib/rodauth/features/disallow_password_reuse.rb +82 -0
  44. data/lib/rodauth/features/email_base.rb +63 -0
  45. data/lib/rodauth/features/jwt.rb +151 -0
  46. data/lib/rodauth/features/lockout.rb +262 -0
  47. data/lib/rodauth/features/login.rb +61 -0
  48. data/lib/rodauth/features/login_password_requirements_base.rb +123 -0
  49. data/lib/rodauth/features/logout.rb +37 -0
  50. data/lib/rodauth/features/otp.rb +338 -0
  51. data/lib/rodauth/features/password_complexity.rb +89 -0
  52. data/lib/rodauth/features/password_expiration.rb +111 -0
  53. data/lib/rodauth/features/password_grace_period.rb +46 -0
  54. data/lib/rodauth/features/recovery_codes.rb +240 -0
  55. data/lib/rodauth/features/remember.rb +200 -0
  56. data/lib/rodauth/features/reset_password.rb +207 -0
  57. data/lib/rodauth/features/session_expiration.rb +55 -0
  58. data/lib/rodauth/features/single_session.rb +87 -0
  59. data/lib/rodauth/features/sms_codes.rb +498 -0
  60. data/lib/rodauth/features/two_factor_base.rb +135 -0
  61. data/lib/rodauth/features/verify_account.rb +232 -0
  62. data/lib/rodauth/features/verify_account_grace_period.rb +76 -0
  63. data/lib/rodauth/features/verify_change_login.rb +20 -0
  64. data/lib/rodauth/migrations.rb +130 -0
  65. data/lib/rodauth/version.rb +9 -0
  66. data/spec/account_expiration_spec.rb +90 -0
  67. data/spec/all.rb +1 -0
  68. data/spec/change_login_spec.rb +149 -0
  69. data/spec/change_password_spec.rb +177 -0
  70. data/spec/close_account_spec.rb +162 -0
  71. data/spec/confirm_password_spec.rb +70 -0
  72. data/spec/create_account_spec.rb +127 -0
  73. data/spec/disallow_password_reuse_spec.rb +84 -0
  74. data/spec/lockout_spec.rb +228 -0
  75. data/spec/login_spec.rb +188 -0
  76. data/spec/migrate/001_tables.rb +103 -16
  77. data/spec/migrate/002_account_password_hash_column.rb +11 -0
  78. data/spec/migrate_password/001_tables.rb +60 -42
  79. data/spec/migrate_travis/001_tables.rb +116 -0
  80. data/spec/password_complexity_spec.rb +108 -0
  81. data/spec/password_expiration_spec.rb +243 -0
  82. data/spec/password_grace_period_spec.rb +93 -0
  83. data/spec/remember_spec.rb +424 -0
  84. data/spec/reset_password_spec.rb +185 -0
  85. data/spec/rodauth_spec.rb +57 -980
  86. data/spec/session_expiration_spec.rb +58 -0
  87. data/spec/single_session_spec.rb +107 -0
  88. data/spec/spec_helper.rb +202 -0
  89. data/spec/two_factor_spec.rb +1310 -0
  90. data/spec/verify_account_grace_period_spec.rb +135 -0
  91. data/spec/verify_account_spec.rb +142 -0
  92. data/spec/verify_change_login_spec.rb +46 -0
  93. data/spec/views/login.str +2 -2
  94. data/templates/add-recovery-codes.str +2 -0
  95. data/templates/button.str +5 -0
  96. data/templates/change-login.str +5 -18
  97. data/templates/change-password.str +6 -14
  98. data/templates/close-account.str +3 -6
  99. data/templates/confirm-password.str +4 -14
  100. data/templates/create-account.str +6 -30
  101. data/templates/login-confirm-field.str +6 -0
  102. data/templates/login-field.str +6 -0
  103. data/templates/login.str +5 -19
  104. data/templates/logout.str +2 -6
  105. data/templates/otp-auth-code-field.str +6 -0
  106. data/templates/otp-auth.str +8 -0
  107. data/templates/otp-disable.str +6 -0
  108. data/templates/otp-setup.str +21 -0
  109. data/templates/password-confirm-field.str +6 -0
  110. data/templates/password-field.str +6 -0
  111. data/templates/recovery-auth.str +12 -0
  112. data/templates/recovery-codes.str +6 -0
  113. data/templates/remember.str +8 -12
  114. data/templates/reset-password-request.str +2 -2
  115. data/templates/reset-password.str +4 -18
  116. data/templates/sms-auth.str +6 -0
  117. data/templates/sms-code-field.str +6 -0
  118. data/templates/sms-confirm.str +7 -0
  119. data/templates/sms-disable.str +7 -0
  120. data/templates/sms-request.str +5 -0
  121. data/templates/sms-setup.str +12 -0
  122. data/templates/unlock-account-request.str +3 -7
  123. data/templates/unlock-account.str +4 -7
  124. data/templates/verify-account-resend.str +2 -2
  125. data/templates/verify-account.str +2 -6
  126. metadata +191 -29
  127. data/lib/roda/plugins/rodauth/base.rb +0 -428
  128. data/lib/roda/plugins/rodauth/change_login.rb +0 -48
  129. data/lib/roda/plugins/rodauth/change_password.rb +0 -42
  130. data/lib/roda/plugins/rodauth/close_account.rb +0 -42
  131. data/lib/roda/plugins/rodauth/create_account.rb +0 -92
  132. data/lib/roda/plugins/rodauth/lockout.rb +0 -292
  133. data/lib/roda/plugins/rodauth/login.rb +0 -81
  134. data/lib/roda/plugins/rodauth/logout.rb +0 -36
  135. data/lib/roda/plugins/rodauth/remember.rb +0 -226
  136. data/lib/roda/plugins/rodauth/reset_password.rb +0 -205
  137. data/lib/roda/plugins/rodauth/verify_account.rb +0 -228
@@ -0,0 +1,58 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe 'Rodauth session expiration feature' do
4
+ it "should expire sessions based on last activity and max lifetime checks" do
5
+ inactivity = max_lifetime = 300
6
+ expiration_default = true
7
+ rodauth do
8
+ enable :login, :session_expiration
9
+ session_expiration_default{expiration_default}
10
+ session_inactivity_timeout{inactivity}
11
+ max_session_lifetime{max_lifetime}
12
+ end
13
+ roda do |r|
14
+ rodauth.check_session_expiration
15
+ r.rodauth
16
+ r.get("remove-creation"){session.delete(:session_created_at); r.redirect '/'}
17
+ r.get("set-creation"){session[:session_created_at] = Time.now.to_i - 100000; r.redirect '/'}
18
+ r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
19
+ end
20
+
21
+ visit '/'
22
+ page.body.must_include "Not Logged"
23
+
24
+ login
25
+ page.body.must_include "Logged In"
26
+
27
+ inactivity = -1
28
+ visit '/'
29
+ page.title.must_equal 'Login'
30
+ page.find('#error_flash').text.must_equal "This session has expired, please login again."
31
+
32
+ login
33
+ page.title.must_equal 'Login'
34
+ page.find('#error_flash').text.must_equal "This session has expired, please login again."
35
+
36
+ inactivity = 10
37
+ login
38
+ page.body.must_include "Logged In"
39
+
40
+ visit '/set-creation'
41
+ page.title.must_equal 'Login'
42
+ page.find('#error_flash').text.must_equal "This session has expired, please login again."
43
+
44
+ login
45
+ page.body.must_include "Logged In"
46
+
47
+ visit '/remove-creation'
48
+ page.title.must_equal 'Login'
49
+ page.find('#error_flash').text.must_equal "This session has expired, please login again."
50
+
51
+ expiration_default = false
52
+ login
53
+ page.body.must_include "Logged In"
54
+
55
+ visit '/remove-creation'
56
+ page.body.must_include "Logged In"
57
+ end
58
+ end
@@ -0,0 +1,107 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe 'Rodauth single session feature' do
4
+ it "should limit accounts to a single logged in session" do
5
+ rodauth do
6
+ enable :login, :logout, :single_session
7
+ end
8
+ roda do |r|
9
+ rodauth.check_single_session
10
+ r.rodauth
11
+ r.is("clear"){session.delete(:single_session_key); DB[:account_session_keys].delete; r.redirect '/'}
12
+ r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
13
+ end
14
+
15
+ login
16
+ page.body.must_include "Logged In"
17
+
18
+ session1 = get_cookie('rack.session')
19
+
20
+ logout
21
+
22
+ visit '/'
23
+ page.body.must_include "Not Logged"
24
+
25
+ remove_cookie('rack.session')
26
+ set_cookie('rack.session', session1)
27
+ visit '/foo'
28
+ page.current_path.must_equal '/'
29
+ page.body.must_include "Not Logged"
30
+ page.find('#error_flash').text.must_equal "This session has been logged out as another session has become active"
31
+
32
+ login
33
+ page.body.must_include "Logged In"
34
+
35
+ session2 = get_cookie('rack.session')
36
+ remove_cookie('rack.session')
37
+ set_cookie('rack.session', session1)
38
+ visit '/'
39
+ page.body.must_include "Not Logged"
40
+ page.find('#error_flash').text.must_equal "This session has been logged out as another session has become active"
41
+
42
+ remove_cookie('rack.session')
43
+ set_cookie('rack.session', session2)
44
+ visit '/'
45
+ page.body.must_include "Logged In"
46
+
47
+ visit '/clear'
48
+ page.current_path.must_equal '/'
49
+ page.body.must_include "Logged In"
50
+ end
51
+
52
+ it "should limit accounts to a single logged in session" do
53
+ rodauth do
54
+ enable :login, :close_account, :single_session
55
+ close_account_requires_password? false
56
+ end
57
+ roda do |r|
58
+ rodauth.check_single_session
59
+ r.rodauth
60
+ r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
61
+ end
62
+
63
+ login
64
+
65
+ DB[:account_session_keys].count.must_equal 1
66
+ visit '/close-account'
67
+ click_button 'Close Account'
68
+ DB[:account_session_keys].count.must_equal 0
69
+ end
70
+
71
+ it "should limit accounts to a single logged in session when using jwt" do
72
+ rodauth do
73
+ enable :login, :logout, :single_session
74
+ end
75
+ roda(:jwt) do |r|
76
+ rodauth.check_single_session
77
+ r.rodauth
78
+ r.post("clear"){rodauth.session.delete(:single_session_key); DB[:account_session_keys].delete; [3]}
79
+ rodauth.logged_in? ? [1] : [2]
80
+ end
81
+
82
+ json_login
83
+ authorization1 = @authorization
84
+ json_logout
85
+
86
+ json_request.must_equal [200, [2]]
87
+ @authorization = authorization1
88
+ json_request.must_equal [400, {'error'=>"This session has been logged out as another session has become active"}]
89
+
90
+ json_login
91
+ json_request.must_equal [200, [1]]
92
+
93
+ authorization2 = @authorization
94
+ @authorization = authorization1
95
+ json_request.must_equal [400, {'error'=>"This session has been logged out as another session has become active"}]
96
+
97
+ @authorization = authorization2
98
+ json_request.must_equal [200, [1]]
99
+
100
+ json_request('/clear').must_equal [200, [3]]
101
+ json_request.must_equal [400, {'error'=>"This session has been logged out as another session has become active"}]
102
+ json_request.must_equal [200, [2]]
103
+
104
+ @authorization = authorization2
105
+ json_request.must_equal [400, {'error'=>"This session has been logged out as another session has become active"}]
106
+ end
107
+ end
@@ -0,0 +1,202 @@
1
+ $: << 'lib'
2
+
3
+ if ENV['COVERAGE']
4
+ require 'coverage'
5
+ require 'simplecov'
6
+
7
+ def SimpleCov.rodauth_coverage(opts = {})
8
+ start do
9
+ add_filter "/spec/"
10
+ add_group('Missing'){|src| src.covered_percent < 100}
11
+ add_group('Covered'){|src| src.covered_percent == 100}
12
+ yield self if block_given?
13
+ end
14
+ end
15
+
16
+ ENV.delete('COVERAGE')
17
+ SimpleCov.rodauth_coverage
18
+ end
19
+
20
+ require 'rubygems'
21
+ require 'capybara'
22
+ require 'capybara/dsl'
23
+ require 'rack/test'
24
+ require 'stringio'
25
+ gem 'minitest'
26
+ require 'minitest/autorun'
27
+ require 'minitest/hooks/default'
28
+
29
+ require 'roda'
30
+ require 'sequel'
31
+ require 'bcrypt'
32
+ require 'mail'
33
+ require 'logger'
34
+ require 'tilt/string'
35
+
36
+ db_url = ENV['RODAUTH_SPEC_DB'] || 'postgres:///?user=rodauth_test&password=rodauth_test'
37
+ DB = Sequel.connect(db_url)
38
+ puts "using #{DB.database_type}"
39
+
40
+ #DB.loggers << Logger.new($stdout)
41
+ if DB.adapter_scheme == :jdbc && DB.database_type == :postgres
42
+ DB.add_named_conversion_proc(:citext){|s| s}
43
+ end
44
+ if DB.adapter_scheme == :jdbc && DB.database_type == :sqlite
45
+ DB.timezone = :utc
46
+ Sequel.application_timezone = :local
47
+ end
48
+ if ENV['RODAUTH_SPEC_MIGRATE']
49
+ Sequel.extension :migration
50
+ Sequel::Migrator.run(DB, 'spec/migrate_travis')
51
+ end
52
+
53
+ ENV['RACK_ENV'] = 'test'
54
+
55
+ ::Mail.defaults do
56
+ delivery_method :test
57
+ end
58
+
59
+ Base = Class.new(Roda)
60
+ Base.plugin :render, :layout=>{:path=>'spec/views/layout.str'}
61
+ Base.plugin(:not_found){raise "path #{request.path_info} not found"}
62
+ Base.use Rack::Session::Cookie, :secret=>'0123456789'
63
+ class Base
64
+ attr_writer :title
65
+ end
66
+
67
+ JsonBase = Class.new(Roda)
68
+ JsonBase.plugin(:not_found){raise "path #{request.path_info} not found"}
69
+
70
+ class Minitest::HooksSpec
71
+ include Rack::Test::Methods
72
+ include Capybara::DSL
73
+
74
+ attr_reader :app
75
+
76
+ def no_freeze!
77
+ @no_freeze = true
78
+ end
79
+
80
+ def app=(app)
81
+ @app = Capybara.app = app
82
+ end
83
+
84
+ def rodauth(&block)
85
+ @rodauth_block = block
86
+ end
87
+
88
+ def roda(type=nil, &block)
89
+ jwt = type == :jwt
90
+ app = Class.new(jwt ? JsonBase : Base)
91
+ rodauth_block = @rodauth_block
92
+ opts = {}
93
+ opts[:json] = :only if jwt
94
+ app.plugin(:rodauth, opts) do
95
+ title_instance_variable :@title
96
+ if jwt
97
+ enable :jwt
98
+ jwt_secret '1'
99
+ json_response_success_key 'success'
100
+ end
101
+ instance_exec(&rodauth_block)
102
+ end
103
+ app.route(&block)
104
+ app.freeze unless @no_freeze
105
+ self.app = app
106
+ end
107
+
108
+ def email_link(regexp, to='foo@example.com')
109
+ msgs = Mail::TestMailer.deliveries
110
+ msgs.length.must_equal 1
111
+ msgs.first.to.first.must_equal to
112
+
113
+ link = msgs.first.body.to_s[regexp]
114
+ msgs.clear
115
+ link.must_be_kind_of(String)
116
+ link
117
+ end
118
+
119
+ def remove_cookie(key)
120
+ page.driver.browser.rack_mock_session.cookie_jar.delete(key)
121
+ end
122
+
123
+ def get_cookie(key)
124
+ page.driver.browser.rack_mock_session.cookie_jar[key]
125
+ end
126
+
127
+ def set_cookie(key, value)
128
+ page.driver.browser.rack_mock_session.cookie_jar[key] = value
129
+ end
130
+
131
+ def json_request(path='/', params={})
132
+ env = {"REQUEST_METHOD" => params.delete(:method) || "POST",
133
+ "PATH_INFO" => path,
134
+ "SCRIPT_NAME" => "",
135
+ "CONTENT_TYPE" => params.delete(:content_type) || "application/json",
136
+ "SERVER_NAME" => 'example.com',
137
+ "rack.input"=>StringIO.new((params || {}).to_json)
138
+ }
139
+
140
+ if @authorization
141
+ env["HTTP_AUTHORIZATION"] = "Bearer: #{@authorization}"
142
+ end
143
+ if @cookie
144
+ env["HTTP_COOKIE"] = @cookie
145
+ end
146
+
147
+ r = @app.call(env)
148
+
149
+ if cookie = r[1]['Set-Cookie']
150
+ @cookie = cookie
151
+ end
152
+ if authorization = r[1]['Authorization']
153
+ @authorization = authorization
154
+ end
155
+ [r[0], JSON.parse("[#{r[2].join}]").first]
156
+ end
157
+
158
+ def json_login(opts={})
159
+ res = json_request(opts[:path]||'/login', :login=>opts[:login]||'foo@example.com', :password=>opts[:pass]||'0123456789')
160
+ res.must_equal [200, {"success"=>'You have been logged in'}] unless opts[:no_check]
161
+ res
162
+ end
163
+
164
+ def json_logout
165
+ json_request("/logout").must_equal [200, {"success"=>'You have been logged out'}]
166
+ end
167
+
168
+ def login(opts={})
169
+ visit(opts[:path]||'/login') unless opts[:visit] == false
170
+ fill_in 'Login', :with=>opts[:login]||'foo@example.com'
171
+ fill_in 'Password', :with=>opts[:pass]||'0123456789'
172
+ click_button 'Login'
173
+ end
174
+
175
+ def logout
176
+ visit '/logout'
177
+ click_button 'Logout'
178
+ end
179
+
180
+ around do |&block|
181
+ DB.transaction(:rollback=>:always, :savepoint=>true, :auto_savepoint=>true){super(&block)}
182
+ end
183
+
184
+ around(:all) do |&block|
185
+ DB.transaction(:rollback=>:always) do
186
+ hash = BCrypt::Password.create('0123456789', :cost=>BCrypt::Engine::MIN_COST)
187
+ DB[:account_password_hashes].insert(:id=>DB[:accounts].insert(:email=>'foo@example.com', :status_id=>2, :ph=>hash), :password_hash=>hash)
188
+ super(&block)
189
+ end
190
+ end
191
+
192
+ after do
193
+ msgs = Mail::TestMailer.deliveries
194
+ len = msgs.length
195
+ msgs.clear
196
+ len.must_equal 0
197
+ Capybara.reset_sessions!
198
+ Capybara.use_default_driver
199
+ end
200
+ end
201
+
202
+
@@ -0,0 +1,1310 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe 'Rodauth OTP feature' do
4
+ def reset_otp_last_use
5
+ DB[:account_otp_keys].update(:last_use=>Sequel.date_sub(Sequel::CURRENT_TIMESTAMP, :seconds=>600))
6
+ end
7
+
8
+ it "should allow two factor authentication setup, login, recovery, removal" do
9
+ sms_phone = sms_message = nil
10
+ rodauth do
11
+ enable :login, :logout, :otp, :recovery_codes, :sms_codes
12
+ sms_send do |phone, msg|
13
+ proc{super(phone, msg)}.must_raise NotImplementedError
14
+ sms_phone = phone
15
+ sms_message = msg
16
+ end
17
+ end
18
+ roda do |r|
19
+ r.rodauth
20
+
21
+ r.redirect '/login' unless rodauth.logged_in?
22
+
23
+ if rodauth.two_factor_authentication_setup?
24
+ r.redirect '/otp-auth' unless rodauth.authenticated?
25
+ view :content=>"With OTP"
26
+ else
27
+ view :content=>"Without OTP"
28
+ end
29
+ end
30
+
31
+ login
32
+ page.html.must_include('Without OTP')
33
+
34
+ %w'/otp-disable /recovery-auth /recovery-codes /sms-setup /sms-disable /sms-confirm /sms-request /sms-auth /otp-auth'.each do |path|
35
+ visit path
36
+ page.find('#error_flash').text.must_equal 'This account has not been setup for two factor authentication'
37
+ page.current_path.must_equal '/otp-setup'
38
+ end
39
+
40
+ page.title.must_equal 'Setup Two Factor Authentication'
41
+ page.html.must_include '<svg'
42
+ secret = page.html.match(/Secret: ([a-z2-7]{16})/)[1]
43
+ totp = ROTP::TOTP.new(secret)
44
+ fill_in 'Password', :with=>'asdf'
45
+ click_button 'Setup Two Factor Authentication'
46
+ page.find('#error_flash').text.must_equal 'Error setting up two factor authentication'
47
+ page.html.must_include 'invalid password'
48
+
49
+ fill_in 'Password', :with=>'0123456789'
50
+ fill_in 'Authentication Code', :with=>"asdf"
51
+ click_button 'Setup Two Factor Authentication'
52
+ page.find('#error_flash').text.must_equal 'Error setting up two factor authentication'
53
+ page.html.must_include 'Invalid authentication code'
54
+
55
+ fill_in 'Password', :with=>'0123456789'
56
+ fill_in 'Authentication Code', :with=>totp.now
57
+ click_button 'Setup Two Factor Authentication'
58
+ page.find('#notice_flash').text.must_equal 'Two factor authentication is now setup'
59
+ page.current_path.must_equal '/'
60
+ page.html.must_include 'With OTP'
61
+
62
+ logout
63
+ login
64
+ page.current_path.must_equal '/otp-auth'
65
+
66
+ %w'/otp-disable /recovery-codes /otp-setup /sms-setup /sms-disable /sms-confirm'.each do |path|
67
+ visit path
68
+ page.find('#error_flash').text.must_equal 'You need to authenticate via 2nd factor before continuing.'
69
+ page.current_path.must_equal '/otp-auth'
70
+ end
71
+
72
+ page.title.must_equal 'Enter Authentication Code'
73
+ fill_in 'Authentication Code', :with=>"asdf"
74
+ click_button 'Authenticate via 2nd Factor'
75
+ page.find('#error_flash').text.must_equal 'Error logging in via two factor authentication'
76
+ page.html.must_include 'Invalid authentication code'
77
+
78
+ fill_in 'Authentication Code', :with=>"#{totp.now[0..2]} #{totp.now[3..-1]}"
79
+ click_button 'Authenticate via 2nd Factor'
80
+ page.find('#error_flash').text.must_equal 'Error logging in via two factor authentication'
81
+ page.html.must_include 'Invalid authentication code'
82
+ reset_otp_last_use
83
+
84
+ fill_in 'Authentication Code', :with=>"#{totp.now[0..2]} #{totp.now[3..-1]}"
85
+ click_button 'Authenticate via 2nd Factor'
86
+ page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
87
+ page.html.must_include 'With OTP'
88
+ reset_otp_last_use
89
+
90
+ visit '/otp-setup'
91
+ page.find('#error_flash').text.must_equal 'You have already setup two factor authentication'
92
+
93
+ %w'/otp-auth /recovery-auth /sms-request /sms-auth'.each do |path|
94
+ visit path
95
+ page.find('#error_flash').text.must_equal 'Already authenticated via 2nd factor'
96
+ end
97
+
98
+ visit '/sms-disable'
99
+ page.find('#error_flash').text.must_equal 'SMS authentication has not been setup yet.'
100
+
101
+ visit '/sms-setup'
102
+ page.title.must_equal 'Setup SMS Backup Number'
103
+ fill_in 'Password', :with=>'012345678'
104
+ fill_in 'Phone Number', :with=>'(123) 456'
105
+ click_button 'Setup SMS Backup Number'
106
+ page.find('#error_flash').text.must_equal 'Error setting up SMS authentication'
107
+ page.html.must_include 'invalid password'
108
+
109
+ fill_in 'Password', :with=>'0123456789'
110
+ click_button 'Setup SMS Backup Number'
111
+ page.find('#error_flash').text.must_equal 'Error setting up SMS authentication'
112
+ page.html.must_include 'invalid SMS phone number'
113
+
114
+ fill_in 'Password', :with=>'0123456789'
115
+ fill_in 'Phone Number', :with=>'(123) 456-7890'
116
+ click_button 'Setup SMS Backup Number'
117
+ page.find('#notice_flash').text.must_equal 'SMS authentication needs confirmation.'
118
+ sms_phone.must_equal '1234567890'
119
+ sms_message.must_match(/\ASMS confirmation code for www\.example\.com is \d{12}\z/)
120
+
121
+ page.title.must_equal 'Confirm SMS Backup Number'
122
+ fill_in 'SMS Code', :with=>"asdf"
123
+ click_button 'Confirm SMS Backup Number'
124
+ page.find('#error_flash').text.must_equal 'Invalid or out of date SMS confirmation code used, must setup SMS authentication again.'
125
+
126
+ fill_in 'Password', :with=>'0123456789'
127
+ fill_in 'Phone Number', :with=>'(123) 456-7890'
128
+ click_button 'Setup SMS Backup Number'
129
+
130
+ visit '/sms-setup'
131
+ page.find('#error_flash').text.must_equal 'SMS authentication needs confirmation.'
132
+ page.title.must_equal 'Confirm SMS Backup Number'
133
+
134
+ DB[:account_sms_codes].update(:code_issued_at=>Time.now - 310)
135
+ sms_code = sms_message[/\d{12}\z/]
136
+ fill_in 'SMS Code', :with=>sms_code
137
+ click_button 'Confirm SMS Backup Number'
138
+ page.find('#error_flash').text.must_equal 'Invalid or out of date SMS confirmation code used, must setup SMS authentication again.'
139
+
140
+ fill_in 'Password', :with=>'0123456789'
141
+ fill_in 'Phone Number', :with=>'(123) 456-7890'
142
+ click_button 'Setup SMS Backup Number'
143
+ sms_code = sms_message[/\d{12}\z/]
144
+ fill_in 'SMS Code', :with=>sms_code
145
+ click_button 'Confirm SMS Backup Number'
146
+ page.find('#notice_flash').text.must_equal 'SMS authentication has been setup.'
147
+
148
+ %w'/sms-setup /sms-confirm'.each do |path|
149
+ visit path
150
+ page.find('#error_flash').text.must_equal 'SMS authentication has already been setup.'
151
+ page.current_path.must_equal '/'
152
+ end
153
+
154
+ logout
155
+ login
156
+
157
+ visit '/sms-auth'
158
+ page.current_path.must_equal '/sms-request'
159
+ page.find('#error_flash').text.must_equal 'No current SMS code for this account'
160
+
161
+ sms_phone = sms_message = nil
162
+ page.title.must_equal 'Send SMS Code'
163
+ click_button 'Send SMS Code'
164
+ sms_phone.must_equal '1234567890'
165
+ sms_message.must_match(/\ASMS authentication code for www\.example\.com is \d{6}\z/)
166
+ sms_code = sms_message[/\d{6}\z/]
167
+
168
+ fill_in 'SMS Code', :with=>"asdf"
169
+ click_button 'Authenticate via SMS Code'
170
+ page.html.must_include 'invalid SMS code'
171
+ page.find('#error_flash').text.must_equal 'Error authenticating via SMS code.'
172
+
173
+ DB[:account_sms_codes].update(:code_issued_at=>Time.now - 310)
174
+ fill_in 'SMS Code', :with=>sms_code
175
+ click_button 'Authenticate via SMS Code'
176
+ page.find('#error_flash').text.must_equal 'No current SMS code for this account'
177
+
178
+ click_button 'Send SMS Code'
179
+ sms_code = sms_message[/\d{6}\z/]
180
+ fill_in 'SMS Code', :with=>sms_code
181
+ click_button 'Authenticate via SMS Code'
182
+ page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
183
+
184
+ logout
185
+ login
186
+
187
+ visit '/sms-request'
188
+ click_button 'Send SMS Code'
189
+
190
+ 5.times do
191
+ click_button 'Authenticate via SMS Code'
192
+ page.find('#error_flash').text.must_equal 'Error authenticating via SMS code.'
193
+ page.current_path.must_equal '/sms-auth'
194
+ end
195
+
196
+ click_button 'Authenticate via SMS Code'
197
+ page.find('#error_flash').text.must_equal 'SMS authentication has been locked out.'
198
+ page.current_path.must_equal '/otp-auth'
199
+
200
+ visit '/sms-request'
201
+ page.find('#error_flash').text.must_equal 'SMS authentication has been locked out.'
202
+ page.current_path.must_equal '/otp-auth'
203
+
204
+ fill_in 'Authentication Code', :with=>totp.now
205
+ click_button 'Authenticate via 2nd Factor'
206
+
207
+ visit '/sms-disable'
208
+ page.title.must_equal 'Disable Backup SMS Authentication'
209
+ fill_in 'Password', :with=>'012345678'
210
+ click_button 'Disable Backup SMS Authentication'
211
+ page.find('#error_flash').text.must_equal 'Error disabling SMS authentication'
212
+ page.html.must_include 'invalid password'
213
+
214
+ fill_in 'Password', :with=>'0123456789'
215
+ click_button 'Disable Backup SMS Authentication'
216
+ page.find('#notice_flash').text.must_equal 'SMS authentication has been disabled.'
217
+ page.current_path.must_equal '/'
218
+
219
+ visit '/sms-setup'
220
+ page.title.must_equal 'Setup SMS Backup Number'
221
+ fill_in 'Password', :with=>'0123456789'
222
+ fill_in 'Phone Number', :with=>'(123) 456-7890'
223
+ click_button 'Setup SMS Backup Number'
224
+ sms_code = sms_message[/\d{12}\z/]
225
+ fill_in 'SMS Code', :with=>sms_code
226
+ click_button 'Confirm SMS Backup Number'
227
+
228
+ visit '/recovery-codes'
229
+ page.title.must_equal 'View Authentication Recovery Codes'
230
+ fill_in 'Password', :with=>'012345678'
231
+ click_button 'View Authentication Recovery Codes'
232
+ page.find('#error_flash').text.must_equal 'Unable to view recovery codes.'
233
+ page.html.must_include 'invalid password'
234
+
235
+ fill_in 'Password', :with=>'0123456789'
236
+ click_button 'View Authentication Recovery Codes'
237
+ page.title.must_equal 'Authentication Recovery Codes'
238
+ recovery_codes = find('#recovery-codes').text.split
239
+ recovery_codes.length.must_equal 16
240
+ recovery_code = recovery_codes.first
241
+
242
+ logout
243
+ login
244
+
245
+ 5.times do
246
+ page.title.must_equal 'Enter Authentication Code'
247
+ fill_in 'Authentication Code', :with=>"asdf"
248
+ click_button 'Authenticate via 2nd Factor'
249
+ page.find('#error_flash').text.must_equal 'Error logging in via two factor authentication'
250
+ page.html.must_include 'Invalid authentication code'
251
+ end
252
+
253
+ page.title.must_equal 'Enter Authentication Code'
254
+ fill_in 'Authentication Code', :with=>"asdf"
255
+ click_button 'Authenticate via 2nd Factor'
256
+ page.find('#error_flash').text.must_equal 'Authentication code use locked out due to numerous failures. Can use recovery code to unlock. Can use SMS code to unlock.'
257
+
258
+ click_button 'Send SMS Code'
259
+
260
+ 5.times do
261
+ click_button 'Authenticate via SMS Code'
262
+ page.find('#error_flash').text.must_equal 'Error authenticating via SMS code.'
263
+ end
264
+
265
+ click_button 'Authenticate via SMS Code'
266
+ page.find('#error_flash').text.must_equal 'Authentication code use locked out due to numerous failures. Can use recovery code to unlock.'
267
+
268
+ page.title.must_equal 'Enter Authentication Recovery Code'
269
+ fill_in 'Recovery Code', :with=>"asdf"
270
+ click_button 'Authenticate via Recovery Code'
271
+ page.find('#error_flash').text.must_equal 'Error authenticating via recovery code.'
272
+ page.html.must_include 'Invalid recovery code'
273
+
274
+ fill_in 'Recovery Code', :with=>recovery_code
275
+ click_button 'Authenticate via Recovery Code'
276
+ page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
277
+ page.html.must_include 'With OTP'
278
+
279
+ visit '/recovery-codes'
280
+ fill_in 'Password', :with=>'0123456789'
281
+ click_button 'View Authentication Recovery Codes'
282
+ page.title.must_equal 'Authentication Recovery Codes'
283
+ page.html.wont_include(recovery_code)
284
+ find('#recovery-codes').text.split.length.must_equal 15
285
+
286
+ click_button 'Add Authentication Recovery Codes'
287
+ page.find('#error_flash').text.must_equal 'Unable to add recovery codes.'
288
+ page.html.must_include 'invalid password'
289
+
290
+ fill_in 'Password', :with=>'0123456789'
291
+ click_button 'View Authentication Recovery Codes'
292
+ find('#recovery-codes').text.split.length.must_equal 15
293
+ fill_in 'Password', :with=>'0123456789'
294
+ click_button 'Add Authentication Recovery Codes'
295
+ page.find('#notice_flash').text.must_equal 'Additional authentication recovery codes have been added.'
296
+ find('#recovery-codes').text.split.length.must_equal 16
297
+ page.html.wont_include('Add Additional Authentication Recovery Codes')
298
+
299
+ visit '/otp-disable'
300
+ fill_in 'Password', :with=>'012345678'
301
+ click_button 'Disable Two Factor Authentication'
302
+ page.find('#error_flash').text.must_equal 'Error disabling up two factor authentication'
303
+ page.html.must_include 'invalid password'
304
+
305
+ fill_in 'Password', :with=>'0123456789'
306
+ click_button 'Disable Two Factor Authentication'
307
+ page.find('#notice_flash').text.must_equal 'Two factor authentication has been disabled'
308
+ page.html.must_include 'Without OTP'
309
+ [:account_otp_keys, :account_recovery_codes, :account_sms_codes].each do |t|
310
+ DB[t].count.must_equal 0
311
+ end
312
+ end
313
+
314
+ it "should allow namespaced two factor authentication without password requirements" do
315
+ rodauth do
316
+ enable :login, :logout, :otp, :recovery_codes
317
+ two_factor_modifications_require_password? false
318
+ otp_digits 8
319
+ otp_interval 300
320
+ prefix "/auth"
321
+ end
322
+ roda do |r|
323
+ r.on "auth" do
324
+ r.rodauth
325
+ end
326
+
327
+ r.redirect '/auth/login' unless rodauth.logged_in?
328
+
329
+ if rodauth.two_factor_authentication_setup?
330
+ r.redirect '/auth/otp-auth' unless rodauth.two_factor_authenticated?
331
+ view :content=>"With OTP"
332
+ else
333
+ view :content=>"Without OTP"
334
+ end
335
+ end
336
+
337
+ login
338
+ page.html.must_include('Without OTP')
339
+
340
+ %w'/auth/otp-disable /auth/recovery-auth /auth/recovery-codes /auth/otp-auth'.each do
341
+ visit '/auth/otp-disable'
342
+ page.find('#error_flash').text.must_equal 'This account has not been setup for two factor authentication'
343
+ page.current_path.must_equal '/auth/otp-setup'
344
+ end
345
+
346
+ page.title.must_equal 'Setup Two Factor Authentication'
347
+ page.html.must_include '<svg'
348
+ secret = page.html.match(/Secret: ([a-z2-7]{16})/)[1]
349
+ totp = ROTP::TOTP.new(secret, :digits=>8, :interval=>300)
350
+ fill_in 'Authentication Code', :with=>"asdf"
351
+ click_button 'Setup Two Factor Authentication'
352
+ page.find('#error_flash').text.must_equal 'Error setting up two factor authentication'
353
+ page.html.must_include 'Invalid authentication code'
354
+
355
+ fill_in 'Authentication Code', :with=>totp.now
356
+ click_button 'Setup Two Factor Authentication'
357
+ page.find('#notice_flash').text.must_equal 'Two factor authentication is now setup'
358
+ page.current_path.must_equal '/'
359
+ page.html.must_include 'With OTP'
360
+ reset_otp_last_use
361
+
362
+ visit '/auth/logout'
363
+ click_button 'Logout'
364
+ login(:visit=>false)
365
+
366
+ page.current_path.must_equal '/auth/otp-auth'
367
+
368
+ visit '/auth/otp-disable'
369
+ page.find('#error_flash').text.must_equal 'You need to authenticate via 2nd factor before continuing.'
370
+ page.current_path.must_equal '/auth/otp-auth'
371
+
372
+ visit '/auth/recovery-codes'
373
+ page.find('#error_flash').text.must_equal 'You need to authenticate via 2nd factor before continuing.'
374
+ page.current_path.must_equal '/auth/otp-auth'
375
+
376
+ visit '/auth/otp-setup'
377
+ page.find('#error_flash').text.must_equal 'You need to authenticate via 2nd factor before continuing.'
378
+ page.current_path.must_equal '/auth/otp-auth'
379
+
380
+ page.title.must_equal 'Enter Authentication Code'
381
+ fill_in 'Authentication Code', :with=>"asdf"
382
+ click_button 'Authenticate via 2nd Factor'
383
+ page.find('#error_flash').text.must_equal 'Error logging in via two factor authentication'
384
+ page.html.must_include 'Invalid authentication code'
385
+
386
+ fill_in 'Authentication Code', :with=>totp.now
387
+ click_button 'Authenticate via 2nd Factor'
388
+ page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
389
+ page.html.must_include 'With OTP'
390
+ reset_otp_last_use
391
+
392
+ visit '/auth/otp-auth'
393
+ page.find('#error_flash').text.must_equal 'Already authenticated via 2nd factor'
394
+
395
+ visit '/auth/otp-setup'
396
+ page.find('#error_flash').text.must_equal 'You have already setup two factor authentication'
397
+
398
+ visit '/auth/recovery-auth'
399
+ page.find('#error_flash').text.must_equal 'Already authenticated via 2nd factor'
400
+
401
+ visit '/auth/recovery-codes'
402
+ page.title.must_equal 'View Authentication Recovery Codes'
403
+ click_button 'View Authentication Recovery Codes'
404
+ page.title.must_equal 'Authentication Recovery Codes'
405
+ recovery_codes = find('#recovery-codes').text.split
406
+ recovery_codes.length.must_equal 16
407
+ recovery_code = recovery_codes.first
408
+
409
+ visit '/auth/logout'
410
+ click_button 'Logout'
411
+ login(:visit=>false)
412
+
413
+ 5.times do
414
+ page.title.must_equal 'Enter Authentication Code'
415
+ fill_in 'Authentication Code', :with=>"asdf"
416
+ click_button 'Authenticate via 2nd Factor'
417
+ page.find('#error_flash').text.must_equal 'Error logging in via two factor authentication'
418
+ page.html.must_include 'Invalid authentication code'
419
+ end
420
+
421
+ page.title.must_equal 'Enter Authentication Code'
422
+ fill_in 'Authentication Code', :with=>"asdf"
423
+ click_button 'Authenticate via 2nd Factor'
424
+
425
+ page.find('#error_flash').text.must_equal 'Authentication code use locked out due to numerous failures. Can use recovery code to unlock.'
426
+ page.title.must_equal 'Enter Authentication Recovery Code'
427
+ fill_in 'Recovery Code', :with=>"asdf"
428
+ click_button 'Authenticate via Recovery Code'
429
+ page.find('#error_flash').text.must_equal 'Error authenticating via recovery code.'
430
+ page.html.must_include 'Invalid recovery code'
431
+ fill_in 'Recovery Code', :with=>recovery_code
432
+ click_button 'Authenticate via Recovery Code'
433
+ page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
434
+ page.html.must_include 'With OTP'
435
+
436
+ visit '/auth/recovery-codes'
437
+ click_button 'View Authentication Recovery Codes'
438
+ page.title.must_equal 'Authentication Recovery Codes'
439
+ page.html.wont_include(recovery_code)
440
+ find('#recovery-codes').text.split.length.must_equal 15
441
+ click_button 'Add Authentication Recovery Codes'
442
+ page.find('#notice_flash').text.must_equal 'Additional authentication recovery codes have been added.'
443
+ find('#recovery-codes').text.split.length.must_equal 16
444
+ page.html.wont_include('Add Additional Authentication Recovery Codes')
445
+
446
+ visit '/auth/otp-disable'
447
+ click_button 'Disable Two Factor Authentication'
448
+ page.find('#notice_flash').text.must_equal 'Two factor authentication has been disabled'
449
+ page.html.must_include 'Without OTP'
450
+ [:account_otp_keys, :account_recovery_codes].each do |t|
451
+ DB[t].count.must_equal 0
452
+ end
453
+ end
454
+
455
+ it "should require login and OTP authentication to perform certain actions if user signed up for OTP" do
456
+ rodauth do
457
+ enable :login, :logout, :change_password, :change_login, :close_account, :otp, :recovery_codes
458
+ end
459
+ roda do |r|
460
+ r.rodauth
461
+
462
+ r.is "a" do
463
+ rodauth.require_authentication
464
+ view(:content=>"a")
465
+ end
466
+
467
+ view(:content=>"b")
468
+ end
469
+
470
+ %w'/change-password /change-login /close-account /a'.each do |path|
471
+ visit '/change-password'
472
+ page.current_path.must_equal '/login'
473
+ end
474
+
475
+ login
476
+
477
+ %w'/change-password /change-login /close-account /a'.each do |path|
478
+ visit path
479
+ page.current_path.must_equal path
480
+ end
481
+
482
+ visit '/otp-setup'
483
+ secret = page.html.match(/Secret: ([a-z2-7]{16})/)[1]
484
+ totp = ROTP::TOTP.new(secret)
485
+ fill_in 'Password', :with=>'0123456789'
486
+ fill_in 'Authentication Code', :with=>totp.now
487
+ click_button 'Setup Two Factor Authentication'
488
+ page.current_path.must_equal '/'
489
+
490
+ logout
491
+ login
492
+
493
+ %w'/change-password /change-login /close-account /a'.each do |path|
494
+ visit path
495
+ page.current_path.must_equal '/otp-auth'
496
+ end
497
+ end
498
+
499
+ it "should handle attempts to insert a duplicate recovery code" do
500
+ keys = ['a', 'a', 'b']
501
+ rodauth do
502
+ enable :login, :logout, :otp, :recovery_codes
503
+ recovery_codes_limit 2
504
+ new_recovery_code{keys.shift}
505
+ end
506
+ roda do |r|
507
+ r.rodauth
508
+
509
+ r.redirect '/login' unless rodauth.logged_in?
510
+
511
+ if rodauth.two_factor_authentication_setup?
512
+ r.redirect '/otp-auth' unless rodauth.authenticated?
513
+ view :content=>"With OTP"
514
+ else
515
+ view :content=>"Without OTP"
516
+ end
517
+ end
518
+
519
+ login
520
+ page.html.must_include('Without OTP')
521
+
522
+ visit '/otp-auth'
523
+ secret = page.html.match(/Secret: ([a-z2-7]{16})/)[1]
524
+ totp = ROTP::TOTP.new(secret)
525
+ fill_in 'Password', :with=>'0123456789'
526
+ fill_in 'Authentication Code', :with=>totp.now
527
+ click_button 'Setup Two Factor Authentication'
528
+ page.find('#notice_flash').text.must_equal 'Two factor authentication is now setup'
529
+ page.current_path.must_equal '/'
530
+ DB[:account_recovery_codes].select_order_map(:code).must_equal ['a', 'b']
531
+ end
532
+
533
+ it "should allow two factor authentication setup, login, removal without recovery" do
534
+ rodauth{enable :login, :logout, :otp}
535
+ roda do |r|
536
+ r.rodauth
537
+
538
+ r.redirect '/login' unless rodauth.logged_in?
539
+
540
+ if rodauth.two_factor_authentication_setup?
541
+ if rodauth.otp_locked_out?
542
+ view :content=>"OTP Locked Out"
543
+ else
544
+ r.redirect '/otp-auth' unless rodauth.authenticated?
545
+ view :content=>"With OTP"
546
+ end
547
+ else
548
+ view :content=>"Without OTP"
549
+ end
550
+ end
551
+
552
+ visit '/recovery-auth'
553
+ page.current_path.must_equal '/login'
554
+ visit '/recovery-codes'
555
+ page.current_path.must_equal '/login'
556
+
557
+ login
558
+ page.html.must_include('Without OTP')
559
+
560
+ visit '/otp-setup'
561
+ page.title.must_equal 'Setup Two Factor Authentication'
562
+ page.html.must_include '<svg'
563
+ secret = page.html.match(/Secret: ([a-z2-7]{16})/)[1]
564
+ totp = ROTP::TOTP.new(secret)
565
+ fill_in 'Password', :with=>'0123456789'
566
+ fill_in 'Authentication Code', :with=>totp.now
567
+ click_button 'Setup Two Factor Authentication'
568
+ page.find('#notice_flash').text.must_equal 'Two factor authentication is now setup'
569
+ page.current_path.must_equal '/'
570
+ page.html.must_include 'With OTP'
571
+ reset_otp_last_use
572
+
573
+ logout
574
+ login
575
+
576
+ visit '/otp-auth'
577
+ 6.times do
578
+ page.title.must_equal 'Enter Authentication Code'
579
+ fill_in 'Authentication Code', :with=>'foo'
580
+ click_button 'Authenticate via 2nd Factor'
581
+ end
582
+ page.find('#error_flash').text.must_equal 'Authentication code use locked out due to numerous failures.'
583
+ page.body.must_include 'OTP Locked Out'
584
+ page.current_path.must_equal '/'
585
+ DB[:account_otp_keys].update(:num_failures=>0)
586
+
587
+ visit '/otp-auth'
588
+ page.title.must_equal 'Enter Authentication Code'
589
+ page.html.wont_include 'Authenticate using recovery code'
590
+ fill_in 'Authentication Code', :with=>totp.now
591
+ click_button 'Authenticate via 2nd Factor'
592
+ page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
593
+ page.html.must_include 'With OTP'
594
+
595
+ visit '/otp-disable'
596
+ fill_in 'Password', :with=>'0123456789'
597
+ click_button 'Disable Two Factor Authentication'
598
+ page.find('#notice_flash').text.must_equal 'Two factor authentication has been disabled'
599
+ page.html.must_include 'Without OTP'
600
+ DB[:account_otp_keys].count.must_equal 0
601
+ end
602
+
603
+ it "should remove otp data when closing accounts" do
604
+ rodauth do
605
+ enable :login, :logout, :otp, :recovery_codes, :sms_codes, :close_account
606
+ two_factor_modifications_require_password? false
607
+ close_account_requires_password? false
608
+ sms_send{|*|}
609
+ end
610
+ roda do |r|
611
+ r.rodauth
612
+ r.root{view :content=>"With OTP"}
613
+ end
614
+
615
+ login
616
+
617
+ visit '/otp-setup'
618
+ secret = page.html.match(/Secret: ([a-z2-7]{16})/)[1]
619
+ totp = ROTP::TOTP.new(secret)
620
+ fill_in 'Authentication Code', :with=>totp.now
621
+ click_button 'Setup Two Factor Authentication'
622
+
623
+ visit '/sms-setup'
624
+ fill_in 'Phone Number', :with=>'(123) 456-7890'
625
+ click_button 'Setup SMS Backup Number'
626
+
627
+ DB[:account_otp_keys].count.must_equal 1
628
+ DB[:account_recovery_codes].count.must_equal 16
629
+ DB[:account_sms_codes].count.must_equal 1
630
+ visit '/close-account'
631
+ click_button 'Close Account'
632
+ [:account_otp_keys, :account_recovery_codes, :account_sms_codes].each do |t|
633
+ DB[t].count.must_equal 0
634
+ end
635
+ end
636
+
637
+ it "should have recovery_codes and sms_codes work when used without otp" do
638
+ sms_code, sms_phone, sms_message = nil
639
+ rodauth do
640
+ enable :login, :logout, :recovery_codes, :sms_codes
641
+ sms_send do |phone, msg|
642
+ sms_phone = phone
643
+ sms_message = msg
644
+ sms_code = msg[/\d+\z/]
645
+ end
646
+ end
647
+ roda do |r|
648
+ r.rodauth
649
+
650
+ r.redirect '/login' unless rodauth.logged_in?
651
+
652
+ if rodauth.two_factor_authentication_setup?
653
+ r.redirect '/sms-request' unless rodauth.authenticated?
654
+ view :content=>"With OTP"
655
+ else
656
+ view :content=>"Without OTP"
657
+ end
658
+ end
659
+
660
+ login
661
+ page.html.must_include('Without OTP')
662
+
663
+ %w'/recovery-auth /recovery-codes'.each do |path|
664
+ visit path
665
+ page.find('#error_flash').text.must_equal 'This account has not been setup for two factor authentication'
666
+ page.current_path.must_equal '/sms-setup'
667
+ end
668
+
669
+ %w'/sms-disable /sms-request /sms-auth'.each do |path|
670
+ visit path
671
+ page.find('#error_flash').text.must_equal 'SMS authentication has not been setup yet.'
672
+ page.current_path.must_equal '/sms-setup'
673
+ end
674
+
675
+ visit '/sms-setup'
676
+ page.title.must_equal 'Setup SMS Backup Number'
677
+ fill_in 'Password', :with=>'012345678'
678
+ fill_in 'Phone Number', :with=>'(123) 456'
679
+ click_button 'Setup SMS Backup Number'
680
+ page.find('#error_flash').text.must_equal 'Error setting up SMS authentication'
681
+ page.html.must_include 'invalid password'
682
+
683
+ fill_in 'Password', :with=>'0123456789'
684
+ click_button 'Setup SMS Backup Number'
685
+ page.find('#error_flash').text.must_equal 'Error setting up SMS authentication'
686
+ page.html.must_include 'invalid SMS phone number'
687
+
688
+ fill_in 'Password', :with=>'0123456789'
689
+ fill_in 'Phone Number', :with=>'(123) 456-7890'
690
+ click_button 'Setup SMS Backup Number'
691
+ page.find('#notice_flash').text.must_equal 'SMS authentication needs confirmation.'
692
+ sms_phone.must_equal '1234567890'
693
+ sms_message.must_match(/\ASMS confirmation code for www\.example\.com is \d{12}\z/)
694
+
695
+ page.title.must_equal 'Confirm SMS Backup Number'
696
+ fill_in 'SMS Code', :with=>"asdf"
697
+ click_button 'Confirm SMS Backup Number'
698
+ page.find('#error_flash').text.must_equal 'Invalid or out of date SMS confirmation code used, must setup SMS authentication again.'
699
+
700
+ fill_in 'Password', :with=>'0123456789'
701
+ fill_in 'Phone Number', :with=>'(123) 456-7890'
702
+ click_button 'Setup SMS Backup Number'
703
+
704
+ visit '/sms-setup'
705
+ page.find('#error_flash').text.must_equal 'SMS authentication needs confirmation.'
706
+ page.title.must_equal 'Confirm SMS Backup Number'
707
+
708
+ DB[:account_sms_codes].update(:code_issued_at=>Time.now - 310)
709
+ fill_in 'SMS Code', :with=>sms_code
710
+ click_button 'Confirm SMS Backup Number'
711
+ page.find('#error_flash').text.must_equal 'Invalid or out of date SMS confirmation code used, must setup SMS authentication again.'
712
+
713
+ fill_in 'Password', :with=>'0123456789'
714
+ fill_in 'Phone Number', :with=>'(123) 456-7890'
715
+ click_button 'Setup SMS Backup Number'
716
+ fill_in 'SMS Code', :with=>sms_code
717
+ click_button 'Confirm SMS Backup Number'
718
+ page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
719
+
720
+ visit '/recovery-codes'
721
+ page.title.must_equal 'View Authentication Recovery Codes'
722
+ fill_in 'Password', :with=>'012345678'
723
+ click_button 'View Authentication Recovery Codes'
724
+ page.find('#error_flash').text.must_equal 'Unable to view recovery codes.'
725
+ page.html.must_include 'invalid password'
726
+
727
+ fill_in 'Password', :with=>'0123456789'
728
+ click_button 'View Authentication Recovery Codes'
729
+ page.title.must_equal 'Authentication Recovery Codes'
730
+ recovery_codes = find('#recovery-codes').text.split
731
+ recovery_codes.length.must_equal 16
732
+ recovery_code = recovery_codes.first
733
+
734
+ logout
735
+ login
736
+ page.current_path.must_equal '/sms-request'
737
+
738
+ %w'/recovery-codes /sms-setup /sms-disable /sms-confirm'.each do |path|
739
+ visit path
740
+ page.find('#error_flash').text.must_equal 'You need to authenticate via 2nd factor before continuing.'
741
+ page.current_path.must_equal '/sms-request'
742
+ end
743
+
744
+ visit '/sms-auth'
745
+ page.current_path.must_equal '/sms-request'
746
+ page.find('#error_flash').text.must_equal 'No current SMS code for this account'
747
+
748
+ sms_phone = sms_message = nil
749
+ page.title.must_equal 'Send SMS Code'
750
+ click_button 'Send SMS Code'
751
+ sms_phone.must_equal '1234567890'
752
+ sms_message.must_match(/\ASMS authentication code for www\.example\.com is \d{6}\z/)
753
+
754
+ fill_in 'SMS Code', :with=>"asdf"
755
+ click_button 'Authenticate via SMS Code'
756
+ page.html.must_include 'invalid SMS code'
757
+ page.find('#error_flash').text.must_equal 'Error authenticating via SMS code.'
758
+
759
+ DB[:account_sms_codes].update(:code_issued_at=>Time.now - 310)
760
+ fill_in 'SMS Code', :with=>sms_code
761
+ click_button 'Authenticate via SMS Code'
762
+ page.find('#error_flash').text.must_equal 'No current SMS code for this account'
763
+
764
+ click_button 'Send SMS Code'
765
+ fill_in 'SMS Code', :with=>sms_code
766
+ click_button 'Authenticate via SMS Code'
767
+ page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
768
+
769
+ %w'/recovery-auth /sms-request /sms-auth'.each do |path|
770
+ visit path
771
+ page.find('#error_flash').text.must_equal 'Already authenticated via 2nd factor'
772
+ end
773
+
774
+ %w'/sms-setup /sms-confirm'.each do |path|
775
+ visit path
776
+ page.find('#error_flash').text.must_equal 'SMS authentication has already been setup.'
777
+ page.current_path.must_equal '/'
778
+ end
779
+
780
+ logout
781
+ login
782
+
783
+ click_button 'Send SMS Code'
784
+
785
+ 5.times do
786
+ click_button 'Authenticate via SMS Code'
787
+ page.find('#error_flash').text.must_equal 'Error authenticating via SMS code.'
788
+ page.current_path.must_equal '/sms-auth'
789
+ end
790
+
791
+ click_button 'Authenticate via SMS Code'
792
+ page.find('#error_flash').text.must_equal 'SMS authentication has been locked out.'
793
+ page.current_path.must_equal '/recovery-auth'
794
+
795
+ visit '/sms-request'
796
+ page.find('#error_flash').text.must_equal 'SMS authentication has been locked out.'
797
+ page.current_path.must_equal '/recovery-auth'
798
+
799
+ page.title.must_equal 'Enter Authentication Recovery Code'
800
+ fill_in 'Recovery Code', :with=>"asdf"
801
+ click_button 'Authenticate via Recovery Code'
802
+ page.find('#error_flash').text.must_equal 'Error authenticating via recovery code.'
803
+ page.html.must_include 'Invalid recovery code'
804
+
805
+ fill_in 'Recovery Code', :with=>recovery_code
806
+ click_button 'Authenticate via Recovery Code'
807
+ page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
808
+ page.html.must_include 'With OTP'
809
+
810
+ visit '/recovery-codes'
811
+ fill_in 'Password', :with=>'0123456789'
812
+ click_button 'View Authentication Recovery Codes'
813
+ page.title.must_equal 'Authentication Recovery Codes'
814
+ page.html.wont_include(recovery_code)
815
+ find('#recovery-codes').text.split.length.must_equal 15
816
+
817
+ click_button 'Add Authentication Recovery Codes'
818
+ page.find('#error_flash').text.must_equal 'Unable to add recovery codes.'
819
+ page.html.must_include 'invalid password'
820
+
821
+ visit '/sms-disable'
822
+ page.title.must_equal 'Disable Backup SMS Authentication'
823
+ fill_in 'Password', :with=>'012345678'
824
+ click_button 'Disable Backup SMS Authentication'
825
+ page.find('#error_flash').text.must_equal 'Error disabling SMS authentication'
826
+ page.html.must_include 'invalid password'
827
+
828
+ fill_in 'Password', :with=>'0123456789'
829
+ click_button 'Disable Backup SMS Authentication'
830
+ page.find('#notice_flash').text.must_equal 'SMS authentication has been disabled.'
831
+ page.current_path.must_equal '/'
832
+
833
+ [:account_recovery_codes, :account_sms_codes].each do |t|
834
+ DB[t].count.must_equal 0
835
+ end
836
+ end
837
+
838
+ it "should have recovery_codes work when used by itself" do
839
+ rodauth do
840
+ enable :login, :logout, :recovery_codes
841
+ end
842
+ roda do |r|
843
+ r.rodauth
844
+
845
+ r.redirect '/login' unless rodauth.logged_in?
846
+
847
+ if rodauth.two_factor_authentication_setup?
848
+ r.redirect '/recovery-auth' unless rodauth.authenticated?
849
+ view :content=>"With OTP"
850
+ else
851
+ view :content=>"Without OTP"
852
+ end
853
+ end
854
+
855
+ login
856
+ page.html.must_include('Without OTP')
857
+
858
+ visit '/recovery-auth'
859
+ page.find('#error_flash').text.must_equal 'This account has not been setup for two factor authentication'
860
+ page.current_path.must_equal '/recovery-codes'
861
+
862
+ page.title.must_equal 'View Authentication Recovery Codes'
863
+ fill_in 'Password', :with=>'012345678'
864
+ click_button 'View Authentication Recovery Codes'
865
+ page.find('#error_flash').text.must_equal 'Unable to view recovery codes.'
866
+ page.html.must_include 'invalid password'
867
+
868
+ fill_in 'Password', :with=>'0123456789'
869
+ click_button 'View Authentication Recovery Codes'
870
+ page.title.must_equal 'Authentication Recovery Codes'
871
+ recovery_codes = find('#recovery-codes').text.split
872
+ recovery_codes.length.must_equal 0
873
+ fill_in 'Password', :with=>'0123456789'
874
+ click_button 'Add Authentication Recovery Codes'
875
+ recovery_codes = find('#recovery-codes').text.split
876
+ recovery_codes.length.must_equal 16
877
+ recovery_code = recovery_codes.first
878
+
879
+ logout
880
+ login
881
+ page.current_path.must_equal '/recovery-auth'
882
+
883
+ visit '/recovery-codes'
884
+ page.find('#error_flash').text.must_equal 'You need to authenticate via 2nd factor before continuing.'
885
+ page.current_path.must_equal '/recovery-auth'
886
+
887
+ page.title.must_equal 'Enter Authentication Recovery Code'
888
+ fill_in 'Recovery Code', :with=>"asdf"
889
+ click_button 'Authenticate via Recovery Code'
890
+ page.find('#error_flash').text.must_equal 'Error authenticating via recovery code.'
891
+ page.html.must_include 'Invalid recovery code'
892
+
893
+ fill_in 'Recovery Code', :with=>recovery_code
894
+ click_button 'Authenticate via Recovery Code'
895
+ page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
896
+ page.html.must_include 'With OTP'
897
+
898
+ visit '/recovery-codes'
899
+ fill_in 'Password', :with=>'0123456789'
900
+ click_button 'View Authentication Recovery Codes'
901
+ page.title.must_equal 'Authentication Recovery Codes'
902
+ page.html.wont_include(recovery_code)
903
+ page.html.wont_include('Add Authentication Recovery Codes')
904
+ find('#recovery-codes').text.split.length.must_equal 16
905
+ end
906
+
907
+ it "should have sms_codes work when used by itself" do
908
+ sms_code, sms_phone, sms_message = nil
909
+ rodauth do
910
+ enable :login, :logout, :sms_codes
911
+ sms_send do |phone, msg|
912
+ sms_phone = phone
913
+ sms_message = msg
914
+ sms_code = msg[/\d+\z/]
915
+ end
916
+ end
917
+ roda do |r|
918
+ r.rodauth
919
+
920
+ r.redirect '/login' unless rodauth.logged_in?
921
+
922
+ if rodauth.two_factor_authentication_setup?
923
+ if rodauth.sms_locked_out?
924
+ view :content=>"With SMS Locked Out"
925
+ else
926
+ rodauth.require_two_factor_authenticated
927
+ view :content=>"With OTP"
928
+ end
929
+ else
930
+ view :content=>"Without OTP"
931
+ end
932
+ end
933
+
934
+ login
935
+ page.html.must_include('Without OTP')
936
+
937
+ %w'/sms-disable /sms-request /sms-auth'.each do |path|
938
+ visit path
939
+ page.find('#error_flash').text.must_equal 'SMS authentication has not been setup yet.'
940
+ page.current_path.must_equal '/sms-setup'
941
+ end
942
+
943
+ visit '/sms-setup'
944
+ page.title.must_equal 'Setup SMS Backup Number'
945
+ fill_in 'Password', :with=>'012345678'
946
+ fill_in 'Phone Number', :with=>'(123) 456'
947
+ click_button 'Setup SMS Backup Number'
948
+ page.find('#error_flash').text.must_equal 'Error setting up SMS authentication'
949
+ page.html.must_include 'invalid password'
950
+
951
+ fill_in 'Password', :with=>'0123456789'
952
+ click_button 'Setup SMS Backup Number'
953
+ page.find('#error_flash').text.must_equal 'Error setting up SMS authentication'
954
+ page.html.must_include 'invalid SMS phone number'
955
+
956
+ fill_in 'Password', :with=>'0123456789'
957
+ fill_in 'Phone Number', :with=>'(123) 456-7890'
958
+ click_button 'Setup SMS Backup Number'
959
+ page.find('#notice_flash').text.must_equal 'SMS authentication needs confirmation.'
960
+ sms_phone.must_equal '1234567890'
961
+ sms_message.must_match(/\ASMS confirmation code for www\.example\.com is \d{12}\z/)
962
+
963
+ page.title.must_equal 'Confirm SMS Backup Number'
964
+ fill_in 'SMS Code', :with=>"asdf"
965
+ click_button 'Confirm SMS Backup Number'
966
+ page.find('#error_flash').text.must_equal 'Invalid or out of date SMS confirmation code used, must setup SMS authentication again.'
967
+
968
+ fill_in 'Password', :with=>'0123456789'
969
+ fill_in 'Phone Number', :with=>'(123) 456-7890'
970
+ click_button 'Setup SMS Backup Number'
971
+
972
+ visit '/sms-setup'
973
+ page.find('#error_flash').text.must_equal 'SMS authentication needs confirmation.'
974
+ page.title.must_equal 'Confirm SMS Backup Number'
975
+
976
+ DB[:account_sms_codes].update(:code_issued_at=>Time.now - 310)
977
+ fill_in 'SMS Code', :with=>sms_code
978
+ click_button 'Confirm SMS Backup Number'
979
+ page.find('#error_flash').text.must_equal 'Invalid or out of date SMS confirmation code used, must setup SMS authentication again.'
980
+
981
+ fill_in 'Password', :with=>'0123456789'
982
+ fill_in 'Phone Number', :with=>'(123) 456-7890'
983
+ click_button 'Setup SMS Backup Number'
984
+ fill_in 'SMS Code', :with=>sms_code
985
+ click_button 'Confirm SMS Backup Number'
986
+ page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
987
+
988
+ logout
989
+ login
990
+ page.current_path.must_equal '/sms-request'
991
+
992
+ %w'/sms-setup /sms-disable /sms-confirm'.each do |path|
993
+ visit path
994
+ page.find('#error_flash').text.must_equal 'You need to authenticate via 2nd factor before continuing.'
995
+ page.current_path.must_equal '/sms-request'
996
+ end
997
+
998
+ visit '/sms-auth'
999
+ page.current_path.must_equal '/sms-request'
1000
+ page.find('#error_flash').text.must_equal 'No current SMS code for this account'
1001
+
1002
+ sms_phone = sms_message = nil
1003
+ page.title.must_equal 'Send SMS Code'
1004
+ click_button 'Send SMS Code'
1005
+ sms_phone.must_equal '1234567890'
1006
+ sms_message.must_match(/\ASMS authentication code for www\.example\.com is \d{6}\z/)
1007
+
1008
+ fill_in 'SMS Code', :with=>"asdf"
1009
+ click_button 'Authenticate via SMS Code'
1010
+ page.html.must_include 'invalid SMS code'
1011
+ page.find('#error_flash').text.must_equal 'Error authenticating via SMS code.'
1012
+
1013
+ DB[:account_sms_codes].update(:code_issued_at=>Time.now - 310)
1014
+ fill_in 'SMS Code', :with=>sms_code
1015
+ click_button 'Authenticate via SMS Code'
1016
+ page.find('#error_flash').text.must_equal 'No current SMS code for this account'
1017
+
1018
+ click_button 'Send SMS Code'
1019
+ fill_in 'SMS Code', :with=>sms_code
1020
+ click_button 'Authenticate via SMS Code'
1021
+ page.find('#notice_flash').text.must_equal 'You have been authenticated via 2nd factor'
1022
+
1023
+ %w'/sms-request /sms-auth'.each do |path|
1024
+ visit path
1025
+ page.find('#error_flash').text.must_equal 'Already authenticated via 2nd factor'
1026
+ end
1027
+
1028
+ %w'/sms-setup /sms-confirm'.each do |path|
1029
+ visit path
1030
+ page.find('#error_flash').text.must_equal 'SMS authentication has already been setup.'
1031
+ page.current_path.must_equal '/'
1032
+ end
1033
+
1034
+ logout
1035
+ login
1036
+
1037
+ click_button 'Send SMS Code'
1038
+
1039
+ 5.times do
1040
+ click_button 'Authenticate via SMS Code'
1041
+ page.find('#error_flash').text.must_equal 'Error authenticating via SMS code.'
1042
+ page.current_path.must_equal '/sms-auth'
1043
+ end
1044
+
1045
+ click_button 'Authenticate via SMS Code'
1046
+ page.body.must_include "With SMS Locked Out"
1047
+ page.find('#error_flash').text.must_equal 'SMS authentication has been locked out.'
1048
+ page.current_path.must_equal '/'
1049
+
1050
+ visit '/sms-request'
1051
+ page.find('#error_flash').text.must_equal 'SMS authentication has been locked out.'
1052
+ page.current_path.must_equal '/'
1053
+
1054
+ DB[:account_sms_codes].update(:num_failures=>0)
1055
+ visit '/sms-request'
1056
+ click_button 'Send SMS Code'
1057
+ fill_in 'SMS Code', :with=>sms_code
1058
+ click_button 'Authenticate via SMS Code'
1059
+
1060
+ visit '/sms-disable'
1061
+ page.title.must_equal 'Disable Backup SMS Authentication'
1062
+ fill_in 'Password', :with=>'012345678'
1063
+ click_button 'Disable Backup SMS Authentication'
1064
+ page.find('#error_flash').text.must_equal 'Error disabling SMS authentication'
1065
+ page.html.must_include 'invalid password'
1066
+
1067
+ fill_in 'Password', :with=>'0123456789'
1068
+ click_button 'Disable Backup SMS Authentication'
1069
+ page.find('#notice_flash').text.must_equal 'SMS authentication has been disabled.'
1070
+ page.current_path.must_equal '/'
1071
+
1072
+ DB[:account_sms_codes].count.must_equal 0
1073
+ end
1074
+
1075
+ it "should allow two factor authentication via jwt" do
1076
+ sms_phone = sms_message = sms_code = nil
1077
+ rodauth do
1078
+ enable :login, :logout, :otp, :recovery_codes, :sms_codes
1079
+ sms_send do |phone, msg|
1080
+ sms_phone = phone
1081
+ sms_message = msg
1082
+ sms_code = msg[/\d+\z/]
1083
+ end
1084
+ end
1085
+ roda(:jwt) do |r|
1086
+ r.rodauth
1087
+
1088
+ if rodauth.logged_in?
1089
+ if rodauth.two_factor_authentication_setup?
1090
+ if rodauth.authenticated?
1091
+ [1]
1092
+ else
1093
+ [2]
1094
+ end
1095
+ else
1096
+ [3]
1097
+ end
1098
+ else
1099
+ [4]
1100
+ end
1101
+ end
1102
+
1103
+ json_request.must_equal [200, [4]]
1104
+ json_login
1105
+ json_request.must_equal [200, [3]]
1106
+
1107
+ %w'/otp-disable /recovery-auth /recovery-codes /sms-setup /sms-confirm /otp-auth'.each do |path|
1108
+ json_request(path).must_equal [400, {'error'=>'This account has not been setup for two factor authentication'}]
1109
+ end
1110
+ %w'/sms-disable /sms-request /sms-auth'.each do |path|
1111
+ json_request(path).must_equal [400, {'error'=>'SMS authentication has not been setup yet.'}]
1112
+ end
1113
+
1114
+ secret = ROTP::Base32.random_base32
1115
+ totp = ROTP::TOTP.new(secret)
1116
+
1117
+ res = json_request('/otp-setup', :password=>'123456', :otp_secret=>secret)
1118
+ res.must_equal [400, {'error'=>'Error setting up two factor authentication', "field-error"=>["password", 'invalid password']}]
1119
+
1120
+ res = json_request('/otp-setup', :password=>'0123456789', :otp=>'adsf', :otp_secret=>secret)
1121
+ res.must_equal [400, {'error'=>'Error setting up two factor authentication', "field-error"=>["otp", 'Invalid authentication code']}]
1122
+
1123
+ res = json_request('/otp-setup', :password=>'0123456789', :otp=>'adsf', :otp_secret=>'asdf')
1124
+ res.must_equal [400, {'error'=>'Error setting up two factor authentication', "field-error"=>["otp_secret", 'invalid secret']}]
1125
+
1126
+ res = json_request('/otp-setup', :password=>'0123456789', :otp=>totp.now, :otp_secret=>secret)
1127
+ res.must_equal [200, {'success'=>'Two factor authentication is now setup'}]
1128
+ reset_otp_last_use
1129
+
1130
+ json_logout
1131
+ json_login
1132
+ json_request.must_equal [200, [2]]
1133
+
1134
+ %w'/otp-disable /recovery-codes /otp-setup /sms-setup /sms-disable /sms-confirm'.each do |path|
1135
+ json_request(path).must_equal [400, {'error'=>'You need to authenticate via 2nd factor before continuing.'}]
1136
+ end
1137
+
1138
+ res = json_request('/otp-auth', :otp=>'adsf')
1139
+ res.must_equal [400, {'error'=>'Error logging in via two factor authentication', "field-error"=>["otp", 'Invalid authentication code']}]
1140
+
1141
+ res = json_request('/otp-auth', :otp=>totp.now)
1142
+ res.must_equal [200, {'success'=>'You have been authenticated via 2nd factor'}]
1143
+ json_request.must_equal [200, [1]]
1144
+ reset_otp_last_use
1145
+
1146
+ res = json_request('/otp-setup')
1147
+ res.must_equal [400, {'error'=>'You have already setup two factor authentication'}]
1148
+
1149
+ %w'/otp-auth /recovery-auth /sms-request /sms-auth'.each do |path|
1150
+ res = json_request(path)
1151
+ res.must_equal [400, {'error'=>'Already authenticated via 2nd factor'}]
1152
+ end
1153
+
1154
+ res = json_request('/sms-disable')
1155
+ res.must_equal [400, {'error'=>'SMS authentication has not been setup yet.'}]
1156
+
1157
+ res = json_request('/sms-setup', :password=>'012345678', "sms-phone"=>'(123) 456')
1158
+ res.must_equal [400, {'error'=>'Error setting up SMS authentication', "field-error"=>["password", 'invalid password']}]
1159
+
1160
+ res = json_request('/sms-setup', :password=>'0123456789', "sms-phone"=>'(123) 456')
1161
+ res.must_equal [400, {'error'=>'Error setting up SMS authentication', "field-error"=>["sms-phone", 'invalid SMS phone number']}]
1162
+
1163
+ res = json_request('/sms-setup', :password=>'0123456789', "sms-phone"=>'(123) 4567 890')
1164
+ res.must_equal [200, {'success'=>'SMS authentication needs confirmation.'}]
1165
+
1166
+ sms_phone.must_equal '1234567890'
1167
+ sms_message.must_match(/\ASMS confirmation code for example\.com: is \d{12}\z/)
1168
+
1169
+ res = json_request('/sms-confirm', :sms_code=>'asdf')
1170
+ res.must_equal [400, {'error'=>'Invalid or out of date SMS confirmation code used, must setup SMS authentication again.'}]
1171
+
1172
+ res = json_request('/sms-setup', :password=>'0123456789', "sms-phone"=>'(123) 4567 890')
1173
+ res.must_equal [200, {'success'=>'SMS authentication needs confirmation.'}]
1174
+
1175
+ DB[:account_sms_codes].update(:code_issued_at=>Time.now - 310)
1176
+ res = json_request('/sms-confirm', :sms_code=>sms_code)
1177
+ res.must_equal [400, {'error'=>'Invalid or out of date SMS confirmation code used, must setup SMS authentication again.'}]
1178
+
1179
+ res = json_request('/sms-setup', :password=>'0123456789', "sms-phone"=>'(123) 4567 890')
1180
+ res.must_equal [200, {'success'=>'SMS authentication needs confirmation.'}]
1181
+
1182
+ res = json_request('/sms-confirm', "sms-code"=>sms_code)
1183
+ res.must_equal [200, {'success'=>'SMS authentication has been setup.'}]
1184
+
1185
+ %w'/sms-setup /sms-confirm'.each do |path|
1186
+ res = json_request(path)
1187
+ res.must_equal [400, {'error'=>'SMS authentication has already been setup.'}]
1188
+ end
1189
+
1190
+ json_logout
1191
+ json_login
1192
+
1193
+ res = json_request('/sms-auth')
1194
+ res.must_equal [400, {'error'=>'No current SMS code for this account'}]
1195
+
1196
+ sms_phone = sms_message = nil
1197
+ res = json_request('/sms-request')
1198
+ res.must_equal [200, {'success'=>'SMS authentication code has been sent.'}]
1199
+ sms_phone.must_equal '1234567890'
1200
+ sms_message.must_match(/\ASMS authentication code for example\.com: is \d{6}\z/)
1201
+
1202
+ res = json_request('/sms-auth')
1203
+ res.must_equal [400, {'error'=>'Error authenticating via SMS code.', "field-error"=>["sms-code", "invalid SMS code"]}]
1204
+
1205
+ DB[:account_sms_codes].update(:code_issued_at=>Time.now - 310)
1206
+ res = json_request('/sms-auth')
1207
+ res.must_equal [400, {'error'=>'No current SMS code for this account'}]
1208
+
1209
+ res = json_request('/sms-request')
1210
+ res.must_equal [200, {'success'=>'SMS authentication code has been sent.'}]
1211
+
1212
+ res = json_request('/sms-auth', 'sms-code'=>sms_code)
1213
+ res.must_equal [200, {'success'=>'You have been authenticated via 2nd factor'}]
1214
+ json_request.must_equal [200, [1]]
1215
+
1216
+ json_logout
1217
+ json_login
1218
+
1219
+ res = json_request('/sms-request')
1220
+ res.must_equal [200, {'success'=>'SMS authentication code has been sent.'}]
1221
+
1222
+ 5.times do
1223
+ res = json_request('/sms-auth')
1224
+ res.must_equal [400, {'error'=>'Error authenticating via SMS code.', "field-error"=>["sms-code", "invalid SMS code"]}]
1225
+ end
1226
+
1227
+ res = json_request('/sms-auth')
1228
+ res.must_equal [400, {'error'=>'SMS authentication has been locked out.'}]
1229
+
1230
+ res = json_request('/sms-request')
1231
+ res.must_equal [400, {'error'=>'SMS authentication has been locked out.'}]
1232
+
1233
+ res = json_request('/otp-auth', :otp=>totp.now)
1234
+ res.must_equal [200, {'success'=>'You have been authenticated via 2nd factor'}]
1235
+ json_request.must_equal [200, [1]]
1236
+
1237
+ res = json_request('/sms-disable', :password=>'012345678')
1238
+ res.must_equal [400, {'error'=>'Error disabling SMS authentication', "field-error"=>["password", 'invalid password']}]
1239
+
1240
+ res = json_request('/sms-disable', :password=>'0123456789')
1241
+ res.must_equal [200, {'success'=>'SMS authentication has been disabled.'}]
1242
+
1243
+ res = json_request('/sms-setup', :password=>'0123456789', "sms-phone"=>'(123) 4567 890')
1244
+ res.must_equal [200, {'success'=>'SMS authentication needs confirmation.'}]
1245
+
1246
+ res = json_request('/sms-confirm', "sms-code"=>sms_code)
1247
+ res.must_equal [200, {'success'=>'SMS authentication has been setup.'}]
1248
+
1249
+ res = json_request('/recovery-codes', :password=>'asdf')
1250
+ res.must_equal [400, {'error'=>'Unable to view recovery codes.', "field-error"=>["password", 'invalid password']}]
1251
+
1252
+ res = json_request('/recovery-codes', :password=>'0123456789')
1253
+ codes = res[1].delete('codes')
1254
+ codes.sort.must_equal DB[:account_recovery_codes].select_map(:code).sort
1255
+ res.must_equal [200, {'success'=>''}]
1256
+
1257
+ json_logout
1258
+ json_login
1259
+
1260
+ 5.times do
1261
+ res = json_request('/otp-auth', :otp=>'asdf')
1262
+ res.must_equal [400, {'error'=>'Error logging in via two factor authentication', "field-error"=>["otp", 'Invalid authentication code']}]
1263
+ end
1264
+
1265
+ res = json_request('/otp-auth', :otp=>'asdf')
1266
+ res.must_equal [400, {'error'=>'Authentication code use locked out due to numerous failures. Can use recovery code to unlock. Can use SMS code to unlock.'}]
1267
+
1268
+ res = json_request('/sms-request')
1269
+ 5.times do
1270
+ res = json_request('/sms-auth')
1271
+ res.must_equal [400, {'error'=>'Error authenticating via SMS code.', "field-error"=>["sms-code", "invalid SMS code"]}]
1272
+ end
1273
+
1274
+ res = json_request('/otp-auth', :otp=>'asdf')
1275
+ res.must_equal [400, {'error'=>'Authentication code use locked out due to numerous failures. Can use recovery code to unlock.'}]
1276
+
1277
+ res = json_request('/sms-auth')
1278
+ res.must_equal [400, {'error'=>'SMS authentication has been locked out.'}]
1279
+
1280
+ res = json_request('/recovery-auth', 'recovery-code'=>'adsf')
1281
+ res.must_equal [400, {'error'=>'Error authenticating via recovery code.', "field-error"=>["recovery-code", "Invalid recovery code"]}]
1282
+
1283
+ res = json_request('/recovery-auth', 'recovery-code'=>codes.first)
1284
+ res.must_equal [200, {'success'=>'You have been authenticated via 2nd factor'}]
1285
+ json_request.must_equal [200, [1]]
1286
+
1287
+ res = json_request('/recovery-codes', :password=>'0123456789')
1288
+ codes2 = res[1].delete('codes')
1289
+ codes2.sort.must_equal codes[1..-1].sort
1290
+ res.must_equal [200, {'success'=>''}]
1291
+
1292
+ res = json_request('/recovery-codes', :password=>'012345678', :add=>'1')
1293
+ res.must_equal [400, {'error'=>'Unable to add recovery codes.', "field-error"=>["password", 'invalid password']}]
1294
+
1295
+ res = json_request('/recovery-codes', :password=>'0123456789', :add=>'1')
1296
+ codes3 = res[1].delete('codes')
1297
+ (codes3 - codes2).length.must_equal 1
1298
+ res.must_equal [200, {'success'=>'Additional authentication recovery codes have been added.'}]
1299
+
1300
+ res = json_request('/otp-disable', :password=>'012345678')
1301
+ res.must_equal [400, {'error'=>'Error disabling up two factor authentication', "field-error"=>["password", 'invalid password']}]
1302
+
1303
+ res = json_request('/otp-disable', :password=>'0123456789')
1304
+ res.must_equal [200, {'success'=>'Two factor authentication has been disabled'}]
1305
+
1306
+ [:account_otp_keys, :account_recovery_codes, :account_sms_codes].each do |t|
1307
+ DB[t].count.must_equal 0
1308
+ end
1309
+ end
1310
+ end