rodauth 0.10.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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