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,61 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Login = Feature.define(:login) do
5
+ notice_flash "You have been logged in"
6
+ error_flash "There was an error logging in"
7
+ view 'login', 'Login'
8
+ after
9
+ after 'login_failure'
10
+ before
11
+ before 'login_attempt'
12
+ additional_form_tags
13
+ button 'Login'
14
+ redirect
15
+
16
+ auth_value_method :login_form_footer, ''
17
+
18
+ route do |r|
19
+ check_already_logged_in
20
+ before_login_route
21
+
22
+ r.get do
23
+ login_view
24
+ end
25
+
26
+ r.post do
27
+ clear_session
28
+
29
+ catch_error do
30
+ unless account_from_login(param(login_param))
31
+ throw_error(login_param, no_matching_login_message)
32
+ end
33
+
34
+ before_login_attempt
35
+
36
+ unless open_account?
37
+ throw_error(login_param, unverified_account_message)
38
+ end
39
+
40
+ unless password_match?(param(password_param))
41
+ after_login_failure
42
+ throw_error(password_param, invalid_password_message)
43
+ end
44
+
45
+ transaction do
46
+ before_login
47
+ update_session
48
+ after_login
49
+ end
50
+ set_notice_flash login_notice_flash
51
+ redirect login_redirect
52
+ end
53
+
54
+ set_error_flash login_error_flash
55
+ login_view
56
+ end
57
+ end
58
+
59
+ attr_reader :login_form_header
60
+ end
61
+ end
@@ -0,0 +1,123 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ LoginPasswordRequirementsBase = Feature.define(:login_password_requirements_base) do
5
+ auth_value_method :login_confirm_param, 'login-confirm'
6
+ auth_value_method :login_minimum_length, 3
7
+ auth_value_method :logins_do_not_match_message, 'logins do not match'
8
+ auth_value_method :password_confirm_param, 'password-confirm'
9
+ auth_value_method :password_minimum_length, 6
10
+ auth_value_method :passwords_do_not_match_message, 'passwords do not match'
11
+ auth_value_method :require_email_address_logins?, true
12
+ auth_value_method :require_login_confirmation?, true
13
+ auth_value_method :require_password_confirmation?, true
14
+ auth_value_method :same_as_existing_password_message, "invalid password, same as current password"
15
+
16
+ auth_value_methods(
17
+ :login_confirm_label,
18
+ :login_does_not_meet_requirements_message,
19
+ :login_too_short_message,
20
+ :password_confirm_label,
21
+ :password_does_not_meet_requirements_message,
22
+ :password_hash_cost,
23
+ :password_too_short_message
24
+ )
25
+
26
+ auth_methods(
27
+ :login_meets_requirements?,
28
+ :password_hash,
29
+ :password_meets_requirements?,
30
+ :set_password
31
+ )
32
+
33
+ def login_confirm_label
34
+ "Confirm #{login_label}"
35
+ end
36
+
37
+ def password_confirm_label
38
+ "Confirm #{password_label}"
39
+ end
40
+
41
+ def login_meets_requirements?(login)
42
+ login_meets_length_requirements?(login) && \
43
+ login_meets_email_requirements?(login)
44
+ end
45
+
46
+ def password_meets_requirements?(password)
47
+ password_meets_length_requirements?(password)
48
+ end
49
+
50
+ def set_password(password)
51
+ hash = password_hash(password)
52
+ if account_password_hash_column
53
+ update_account(account_password_hash_column=>hash)
54
+ elsif password_hash_ds.update(password_hash_column=>hash) == 0
55
+ # This shouldn't raise a uniqueness error, as the update should only fail for a new user,
56
+ # and an existing user shouldn't always havae a valid password hash row. If this does
57
+ # fail, retrying it will cause problems, it will override a concurrently running update
58
+ # with potentially a different password.
59
+ db[password_hash_table].insert(password_hash_id_column=>account_id, password_hash_column=>hash)
60
+ end
61
+ hash
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :login_requirement_message
67
+ attr_reader :password_requirement_message
68
+
69
+ def password_does_not_meet_requirements_message
70
+ "invalid password, does not meet requirements#{" (#{password_requirement_message})" if password_requirement_message}"
71
+ end
72
+
73
+ def password_too_short_message
74
+ "minimum #{password_minimum_length} characters"
75
+ end
76
+
77
+ def login_does_not_meet_requirements_message
78
+ "invalid login#{", #{login_requirement_message}" if login_requirement_message}"
79
+ end
80
+
81
+ def login_too_short_message
82
+ "minimum #{login_minimum_length} characters"
83
+ end
84
+
85
+ def login_meets_length_requirements?(login)
86
+ return true if login_minimum_length <= login.length
87
+ @login_requirement_message = login_too_short_message
88
+ false
89
+ end
90
+
91
+ def login_meets_email_requirements?(login)
92
+ return true unless require_email_address_logins?
93
+ if login =~ /\A[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+\z/
94
+ return true
95
+ end
96
+ @login_requirement_message = 'not a valid email address'
97
+ return false
98
+ end
99
+
100
+ def password_meets_length_requirements?(password)
101
+ return true if password_minimum_length <= password.length
102
+ @password_requirement_message = password_too_short_message
103
+ false
104
+ end
105
+
106
+ if ENV['RACK_ENV'] == 'test'
107
+ def password_hash_cost
108
+ BCrypt::Engine::MIN_COST
109
+ end
110
+ else
111
+ # :nocov:
112
+ def password_hash_cost
113
+ BCrypt::Engine::DEFAULT_COST
114
+ end
115
+ # :nocov:
116
+ end
117
+
118
+ def password_hash(password)
119
+ BCrypt::Password.create(password, :cost=>password_hash_cost)
120
+ end
121
+ end
122
+ end
123
+
@@ -0,0 +1,37 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Logout = Feature.define(:logout) do
5
+ notice_flash "You have been logged out"
6
+ view 'logout', 'Logout'
7
+ additional_form_tags
8
+ before
9
+ after
10
+ button 'Logout'
11
+ redirect{require_login_redirect}
12
+
13
+ auth_methods :logout
14
+
15
+ route do |r|
16
+ before_logout_route
17
+
18
+ r.get do
19
+ logout_view
20
+ end
21
+
22
+ r.post do
23
+ transaction do
24
+ before_logout
25
+ logout
26
+ after_logout
27
+ end
28
+ set_notice_flash logout_notice_flash
29
+ redirect logout_redirect
30
+ end
31
+ end
32
+
33
+ def logout
34
+ clear_session
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,338 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'rotp'
4
+ require 'rqrcode'
5
+
6
+ module Rodauth
7
+ Otp = Feature.define(:otp) do
8
+ depends :two_factor_base
9
+
10
+ additional_form_tags 'otp_disable'
11
+ additional_form_tags 'otp_auth'
12
+ additional_form_tags 'otp_setup'
13
+
14
+ after 'otp_authentication_failure'
15
+ after 'otp_disable'
16
+ after 'otp_setup'
17
+
18
+ before 'otp_authentication'
19
+ before 'otp_setup'
20
+ before 'otp_disable'
21
+ before 'otp_authentication_route'
22
+ before 'otp_setup_route'
23
+ before 'otp_disable_route'
24
+
25
+ button 'Authenticate via 2nd Factor', 'otp_auth'
26
+ button 'Disable Two Factor Authentication', 'otp_disable'
27
+ button 'Setup Two Factor Authentication', 'otp_setup'
28
+
29
+ error_flash "Error disabling up two factor authentication", 'otp_disable'
30
+ error_flash "Error logging in via two factor authentication", 'otp_auth'
31
+ error_flash "Error setting up two factor authentication", 'otp_setup'
32
+ error_flash "You have already setup two factor authentication", :otp_already_setup
33
+
34
+ notice_flash "Two factor authentication has been disabled", 'otp_disable'
35
+ notice_flash "Two factor authentication is now setup", 'otp_setup'
36
+
37
+ redirect :otp_disable
38
+ redirect :otp_already_setup
39
+ redirect :otp_setup
40
+
41
+ view 'otp-disable', 'Disable Two Factor Authentication', 'otp_disable'
42
+ view 'otp-auth', 'Enter Authentication Code', 'otp_auth'
43
+ view 'otp-setup', 'Setup Two Factor Authentication', 'otp_setup'
44
+
45
+ auth_value_method :otp_auth_failures_limit, 5
46
+ auth_value_method :otp_auth_label, 'Authentication Code'
47
+ auth_value_method :otp_auth_param, 'otp'
48
+ auth_value_method :otp_class, ROTP::TOTP
49
+ auth_value_method :otp_digits, nil
50
+ auth_value_method :otp_interval, nil
51
+ auth_value_method :otp_invalid_auth_code_message, "Invalid authentication code"
52
+ auth_value_method :otp_invalid_secret_message, "invalid secret"
53
+ auth_value_method :otp_keys_column, :key
54
+ auth_value_method :otp_keys_id_column, :id
55
+ auth_value_method :otp_keys_failures_column, :num_failures
56
+ auth_value_method :otp_keys_table, :account_otp_keys
57
+ auth_value_method :otp_keys_last_use_column, :last_use
58
+ auth_value_method :otp_setup_param, 'otp_secret'
59
+
60
+ auth_cached_method :otp_key
61
+ auth_cached_method :otp
62
+ private :otp
63
+
64
+ auth_value_methods(
65
+ :otp_auth_form_footer,
66
+ :otp_issuer,
67
+ :otp_lockout_error_flash,
68
+ :otp_lockout_redirect
69
+ )
70
+
71
+ auth_methods(
72
+ :otp,
73
+ :otp_exists?,
74
+ :otp_key,
75
+ :otp_locked_out?,
76
+ :otp_new_secret,
77
+ :otp_provisioning_name,
78
+ :otp_provisioning_uri,
79
+ :otp_qr_code,
80
+ :otp_record_authentication_failure,
81
+ :otp_remove,
82
+ :otp_remove_auth_failures,
83
+ :otp_update_last_use,
84
+ :otp_valid_code?,
85
+ :otp_valid_key?
86
+ )
87
+
88
+ auth_private_methods(
89
+ :otp_add_key,
90
+ :otp_tmp_key
91
+ )
92
+
93
+ route(:otp_auth) do |r|
94
+ require_login
95
+ require_account_session
96
+ require_two_factor_not_authenticated
97
+ require_otp_setup
98
+
99
+ if otp_locked_out?
100
+ set_redirect_error_flash otp_lockout_error_flash
101
+ redirect otp_lockout_redirect
102
+ end
103
+
104
+ before_otp_authentication_route
105
+
106
+ r.get do
107
+ otp_auth_view
108
+ end
109
+
110
+ r.post do
111
+ if otp_valid_code?(param(otp_auth_param)) && otp_update_last_use
112
+ before_otp_authentication
113
+ two_factor_authenticate(:totp)
114
+ end
115
+
116
+ otp_record_authentication_failure
117
+ after_otp_authentication_failure
118
+ set_field_error(otp_auth_param, otp_invalid_auth_code_message)
119
+ set_error_flash otp_auth_error_flash
120
+ otp_auth_view
121
+ end
122
+ end
123
+
124
+ route(:otp_setup) do |r|
125
+ require_account
126
+
127
+ if otp_exists?
128
+ set_redirect_error_flash otp_already_setup_error_flash
129
+ redirect otp_already_setup_redirect
130
+ end
131
+
132
+ before_otp_setup_route
133
+
134
+ r.get do
135
+ otp_tmp_key(otp_new_secret)
136
+ otp_setup_view
137
+ end
138
+
139
+ r.post do
140
+ secret = param(otp_setup_param)
141
+ catch_error do
142
+ unless otp_valid_key?(secret)
143
+ throw_error(otp_setup_param, otp_invalid_secret_message)
144
+ end
145
+ otp_tmp_key(secret)
146
+
147
+ unless two_factor_password_match?(param(password_param))
148
+ throw_error(password_param, invalid_password_message)
149
+ end
150
+
151
+ unless otp_valid_code?(param(otp_auth_param))
152
+ throw_error(otp_auth_param, otp_invalid_auth_code_message)
153
+ end
154
+
155
+ transaction do
156
+ before_otp_setup
157
+ otp_add_key
158
+ two_factor_update_session(:totp)
159
+ after_otp_setup
160
+ end
161
+ set_notice_flash otp_setup_notice_flash
162
+ redirect otp_setup_redirect
163
+ end
164
+
165
+ set_error_flash otp_setup_error_flash
166
+ otp_setup_view
167
+ end
168
+ end
169
+
170
+ route(:otp_disable) do |r|
171
+ require_account
172
+ require_otp_setup
173
+ before_otp_disable_route
174
+
175
+ r.get do
176
+ otp_disable_view
177
+ end
178
+
179
+ r.post do
180
+ if two_factor_password_match?(param(password_param))
181
+ transaction do
182
+ before_otp_disable
183
+ otp_remove
184
+ two_factor_remove_session
185
+ after_otp_disable
186
+ end
187
+ set_notice_flash otp_disable_notice_flash
188
+ redirect otp_disable_redirect
189
+ end
190
+
191
+ set_field_error(password_param, invalid_password_message)
192
+ set_error_flash otp_disable_error_flash
193
+ otp_disable_view
194
+ end
195
+ end
196
+
197
+ def two_factor_authentication_setup?
198
+ super || otp_exists?
199
+ end
200
+
201
+ def two_factor_need_setup_redirect
202
+ "#{prefix}/#{otp_setup_route}"
203
+ end
204
+
205
+ def two_factor_auth_required_redirect
206
+ "#{prefix}/#{otp_auth_route}"
207
+ end
208
+
209
+ def two_factor_remove
210
+ super
211
+ otp_remove
212
+ end
213
+
214
+ def two_factor_remove_auth_failures
215
+ super
216
+ otp_remove_auth_failures
217
+ end
218
+
219
+ def otp_auth_form_footer
220
+ super if defined?(super)
221
+ end
222
+
223
+ def otp_lockout_redirect
224
+ return super if defined?(super)
225
+ default_redirect
226
+ end
227
+
228
+ def otp_lockout_error_flash
229
+ "Authentication code use locked out due to numerous failures.#{super if defined?(super)}"
230
+ end
231
+
232
+ def require_otp_setup
233
+ unless otp_exists?
234
+ set_redirect_error_flash two_factor_not_setup_error_flash
235
+ redirect two_factor_need_setup_redirect
236
+ end
237
+ end
238
+
239
+ def otp_exists?
240
+ !otp_key.nil?
241
+ end
242
+
243
+ def otp_valid_code?(ot_pass)
244
+ if otp_exists?
245
+ otp.verify(ot_pass.gsub(/\s+/, ''))
246
+ end
247
+ end
248
+
249
+ def otp_remove
250
+ otp_key_ds.delete
251
+ super if defined?(super)
252
+ end
253
+
254
+ def otp_add_key
255
+ _otp_add_key(otp_key)
256
+ super if defined?(super)
257
+ end
258
+
259
+ def otp_update_last_use
260
+ otp_key_ds.
261
+ where(Sequel.date_add(otp_keys_last_use_column, :seconds=>(otp_interval||30)) < Sequel::CURRENT_TIMESTAMP).
262
+ update(otp_keys_last_use_column=>Sequel::CURRENT_TIMESTAMP) == 1
263
+ end
264
+
265
+ def otp_record_authentication_failure
266
+ otp_key_ds.update(otp_keys_failures_column=>Sequel.identifier(otp_keys_failures_column) + 1)
267
+ end
268
+
269
+ def otp_remove_auth_failures
270
+ otp_key_ds.update(otp_keys_failures_column=>0)
271
+ end
272
+
273
+ def otp_locked_out?
274
+ otp_key_ds.get(otp_keys_failures_column) >= otp_auth_failures_limit
275
+ end
276
+
277
+ def otp_provisioning_uri
278
+ otp.provisioning_uri(otp_provisioning_name)
279
+ end
280
+
281
+ def otp_issuer
282
+ request.host
283
+ end
284
+
285
+ def otp_provisioning_name
286
+ account[login_column]
287
+ end
288
+
289
+ def otp_qr_code
290
+ RQRCode::QRCode.new(otp_provisioning_uri).as_svg(:module_size=>8)
291
+ end
292
+
293
+ private
294
+
295
+ def clear_cached_otp
296
+ remove_instance_variable(:@otp) if defined?(@otp)
297
+ end
298
+
299
+ def otp_tmp_key(secret)
300
+ _otp_tmp_key(secret)
301
+ clear_cached_otp
302
+ end
303
+
304
+ def otp_valid_key?(secret)
305
+ secret =~ /\A[a-z2-7]{16}\z/
306
+ end
307
+
308
+ def otp_new_secret
309
+ ROTP::Base32.random_base32
310
+ end
311
+
312
+ def _otp_tmp_key(secret)
313
+ @otp_key = secret
314
+ end
315
+
316
+ def _otp_add_key(secret)
317
+ # Uniqueness errors can't be handled here, as we can't be sure the secret provided
318
+ # is the same as the current secret.
319
+ otp_key_ds.insert(otp_keys_id_column=>session_value, otp_keys_column=>secret)
320
+ end
321
+
322
+ def _otp_key
323
+ otp_key_ds.get(otp_keys_column)
324
+ end
325
+
326
+ def _otp
327
+ otp_class.new(otp_key, :issuer=>otp_issuer, :digits=>otp_digits, :interval=>otp_interval)
328
+ end
329
+
330
+ def otp_key_ds
331
+ db[otp_keys_table].where(otp_keys_id_column=>session_value)
332
+ end
333
+
334
+ def use_date_arithmetic?
335
+ true
336
+ end
337
+ end
338
+ end