rodauth 1.19.1 → 1.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +72 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +100 -7
  5. data/doc/base.rdoc +25 -0
  6. data/doc/email_auth.rdoc +1 -1
  7. data/doc/email_base.rdoc +5 -1
  8. data/doc/internals.rdoc +2 -2
  9. data/doc/jwt_refresh.rdoc +35 -0
  10. data/doc/lockout.rdoc +3 -0
  11. data/doc/login_password_requirements_base.rdoc +4 -1
  12. data/doc/otp.rdoc +22 -39
  13. data/doc/recovery_codes.rdoc +15 -28
  14. data/doc/release_notes/1.20.0.txt +175 -0
  15. data/doc/remember.rdoc +3 -0
  16. data/doc/reset_password.rdoc +2 -1
  17. data/doc/single_session.rdoc +3 -0
  18. data/doc/verify_account.rdoc +4 -3
  19. data/doc/verify_login_change.rdoc +1 -1
  20. data/lib/rodauth.rb +33 -4
  21. data/lib/rodauth/features/base.rb +93 -10
  22. data/lib/rodauth/features/change_login.rb +1 -1
  23. data/lib/rodauth/features/confirm_password.rb +1 -1
  24. data/lib/rodauth/features/create_account.rb +2 -2
  25. data/lib/rodauth/features/disallow_password_reuse.rb +5 -3
  26. data/lib/rodauth/features/email_auth.rb +4 -2
  27. data/lib/rodauth/features/email_base.rb +12 -6
  28. data/lib/rodauth/features/jwt.rb +9 -0
  29. data/lib/rodauth/features/jwt_refresh.rb +142 -0
  30. data/lib/rodauth/features/lockout.rb +8 -4
  31. data/lib/rodauth/features/login_password_requirements_base.rb +1 -0
  32. data/lib/rodauth/features/otp.rb +63 -6
  33. data/lib/rodauth/features/recovery_codes.rb +1 -0
  34. data/lib/rodauth/features/remember.rb +20 -2
  35. data/lib/rodauth/features/reset_password.rb +5 -2
  36. data/lib/rodauth/features/single_session.rb +15 -2
  37. data/lib/rodauth/features/verify_account.rb +11 -6
  38. data/lib/rodauth/features/verify_login_change.rb +5 -3
  39. data/lib/rodauth/version.rb +2 -2
  40. data/spec/disallow_password_reuse_spec.rb +115 -28
  41. data/spec/email_auth_spec.rb +2 -2
  42. data/spec/jwt_refresh_spec.rb +256 -0
  43. data/spec/lockout_spec.rb +4 -4
  44. data/spec/login_spec.rb +52 -11
  45. data/spec/migrate/001_tables.rb +10 -0
  46. data/spec/migrate_travis/001_tables.rb +8 -0
  47. data/spec/remember_spec.rb +27 -0
  48. data/spec/reset_password_spec.rb +2 -2
  49. data/spec/rodauth_spec.rb +25 -1
  50. data/spec/single_session_spec.rb +20 -0
  51. data/spec/spec_helper.rb +29 -0
  52. data/spec/two_factor_spec.rb +57 -3
  53. data/spec/verify_account_spec.rb +18 -1
  54. data/spec/verify_login_change_spec.rb +2 -2
  55. data/templates/add-recovery-codes.str +1 -1
  56. data/templates/change-password.str +2 -2
  57. data/templates/login-confirm-field.str +2 -2
  58. data/templates/login-field.str +2 -2
  59. data/templates/otp-auth-code-field.str +2 -2
  60. data/templates/otp-setup.str +4 -3
  61. data/templates/password-confirm-field.str +2 -2
  62. data/templates/password-field.str +2 -2
  63. data/templates/recovery-auth.str +2 -2
  64. data/templates/reset-password-request.str +1 -1
  65. data/templates/sms-code-field.str +2 -2
  66. data/templates/sms-setup.str +2 -2
  67. data/templates/unlock-account-request.str +1 -1
  68. data/templates/unlock-account.str +1 -1
  69. data/templates/verify-account-resend.str +1 -1
  70. metadata +15 -5
@@ -81,7 +81,7 @@ module Rodauth
81
81
  updated = nil
82
82
  raised = raises_uniqueness_violation?{updated = update_account({login_column=>login}, account_ds.exclude(login_column=>login)) == 1}
83
83
  if raised
84
- @login_requirement_message = 'already an account with this login'
84
+ @login_requirement_message = already_an_account_with_this_login_message
85
85
  end
86
86
  updated && !raised
87
87
  end
@@ -16,7 +16,7 @@ module Rodauth
16
16
 
17
17
  auth_methods :confirm_password
18
18
 
19
- route do
19
+ route do |r|
20
20
  require_account
21
21
  before_confirm_password_route
22
22
 
@@ -103,13 +103,13 @@ module Rodauth
103
103
  def new_account(login)
104
104
  @account = _new_account(login)
105
105
  end
106
-
106
+
107
107
  def save_account
108
108
  id = nil
109
109
  raised = raises_uniqueness_violation?{id = db[accounts_table].insert(account)}
110
110
 
111
111
  if raised
112
- @login_requirement_message = 'already an account with this login'
112
+ @login_requirement_message = already_an_account_with_this_login_message
113
113
  end
114
114
 
115
115
  if id
@@ -28,8 +28,10 @@ module Rodauth
28
28
  limit(nil, previous_passwords_to_check).
29
29
  get(previous_password_id_column)
30
30
 
31
- ds.where(Sequel.expr(previous_password_id_column) <= keep_before).
32
- delete
31
+ if keep_before
32
+ ds.where(Sequel.expr(previous_password_id_column) <= keep_before).
33
+ delete
34
+ end
33
35
 
34
36
  # This should never raise uniqueness violations, as it uses a serial primary key
35
37
  ds.insert(previous_password_account_id_column=>account_id, previous_password_hash_column=>hash)
@@ -68,7 +70,7 @@ module Rodauth
68
70
  end
69
71
 
70
72
  def after_create_account
71
- if account_password_hash_column
73
+ if account_password_hash_column && !(respond_to?(:verify_account_set_password?) && verify_account_set_password?)
72
74
  add_previous_password_hash(password_hash(param(password_param)))
73
75
  end
74
76
  super if defined?(super)
@@ -4,10 +4,13 @@ module Rodauth
4
4
  Feature.define(:email_auth, :EmailAuth) do
5
5
  depends :login, :email_base
6
6
 
7
+ def_deprecated_alias :no_matching_email_auth_key_error_flash, :no_matching_email_auth_key_message
8
+
7
9
  notice_flash "An email has been sent to you with a link to login to your account", 'email_auth_email_sent'
8
10
  error_flash "There was an error logging you in"
9
11
  error_flash "There was an error requesting an email link to authenticate", 'email_auth_request'
10
12
  error_flash "An email has recently been sent to you with a link to login", 'email_auth_email_recently_sent'
13
+ error_flash "There was an error logging you in: invalid email authentication key", 'no_matching_email_auth_key'
11
14
  loaded_templates %w'email-auth email-auth-request-form email-auth-email'
12
15
 
13
16
  view 'email-auth', 'Login'
@@ -28,7 +31,6 @@ module Rodauth
28
31
  auth_value_method :email_auth_email_last_sent_column, :email_last_sent
29
32
  auth_value_method :email_auth_skip_resend_email_within, 300
30
33
  auth_value_method :email_auth_table, :account_email_auth_keys
31
- auth_value_method :no_matching_email_auth_key_message, "invalid email authentication key"
32
34
  session_key :email_auth_session_key, :email_auth_key
33
35
 
34
36
  auth_value_methods :force_email_auth?
@@ -81,7 +83,7 @@ module Rodauth
81
83
  email_auth_view
82
84
  else
83
85
  session[email_auth_session_key] = nil
84
- set_redirect_error_flash no_matching_email_auth_key_message
86
+ set_redirect_error_flash no_matching_email_auth_key_error_flash
85
87
  redirect require_login_redirect
86
88
  end
87
89
  end
@@ -4,7 +4,7 @@ module Rodauth
4
4
  Feature.define(:email_base, :EmailBase) do
5
5
  auth_value_method :email_subject_prefix, nil
6
6
  auth_value_method :require_mail?, true
7
- auth_value_method :token_separator, "_"
7
+ auth_value_method :allow_raw_email_token?, false
8
8
 
9
9
  redirect :default_post_email
10
10
 
@@ -45,12 +45,12 @@ module Rodauth
45
45
  account[login_column]
46
46
  end
47
47
 
48
- def split_token(token)
49
- token.split(token_separator, 2)
48
+ def token_link(route, param, key)
49
+ "#{request.base_url}#{prefix}/#{route}?#{param}=#{account_id}#{token_separator}#{convert_email_token_key(key)}"
50
50
  end
51
51
 
52
- def token_link(route, param, key)
53
- "#{request.base_url}#{prefix}/#{route}?#{param}=#{account_id}#{token_separator}#{key}"
52
+ def convert_email_token_key(key)
53
+ convert_token_key(key)
54
54
  end
55
55
 
56
56
  def account_from_key(token, status_id=nil)
@@ -59,7 +59,13 @@ module Rodauth
59
59
 
60
60
  return unless actual = yield(id)
61
61
 
62
- return unless timing_safe_eql?(key, actual)
62
+ unless timing_safe_eql?(key, convert_email_token_key(actual))
63
+ if hmac_secret && allow_raw_email_token?
64
+ return unless timing_safe_eql?(key, actual)
65
+ else
66
+ return
67
+ end
68
+ end
63
69
 
64
70
  ds = account_ds(id)
65
71
  ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
@@ -173,6 +173,15 @@ module Rodauth
173
173
  end
174
174
  end
175
175
 
176
+ def before_otp_setup_route
177
+ super if defined?(super)
178
+ if use_jwt? && otp_keys_use_hmac? && !param_or_nil(otp_setup_raw_param)
179
+ _otp_tmp_key(otp_new_secret)
180
+ json_response[otp_setup_param] = otp_user_key
181
+ json_response[otp_setup_raw_param] = otp_key
182
+ end
183
+ end
184
+
176
185
  def jwt_payload
177
186
  return @jwt_payload if defined?(@jwt_payload)
178
187
  @jwt_payload = JWT.decode(jwt_token, jwt_secret, true, jwt_decode_opts.merge(:algorithm=>jwt_algorithm))[0]
@@ -0,0 +1,142 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ JwtRefresh = Feature.define(:jwt_refresh) do
5
+ depends :jwt
6
+
7
+ after 'refresh_token'
8
+ before 'refresh_token'
9
+
10
+ auth_value_method :jwt_access_token_key, 'access_token'
11
+ auth_value_method :jwt_access_token_not_before_period, 5
12
+ auth_value_method :jwt_access_token_period, 1800
13
+ auth_value_method :jwt_refresh_invalid_token_message, 'invalid JWT refresh token'
14
+ auth_value_method :jwt_refresh_token_account_id_column, :account_id
15
+ auth_value_method :jwt_refresh_token_deadline_column, :deadline
16
+ auth_value_method :jwt_refresh_token_deadline_interval, {:days=>14}
17
+ auth_value_method :jwt_refresh_token_id_column, :id
18
+ auth_value_method :jwt_refresh_token_key, 'refresh_token'
19
+ auth_value_method :jwt_refresh_token_key_column, :key
20
+ auth_value_method :jwt_refresh_token_key_param, 'refresh_token'
21
+ auth_value_method :jwt_refresh_token_table, :account_jwt_refresh_keys
22
+
23
+ auth_private_methods(
24
+ :account_from_refresh_token
25
+ )
26
+
27
+ route do |r|
28
+ r.post do
29
+ if (refresh_token = param_or_nil(jwt_refresh_token_key_param)) && account_from_refresh_token(refresh_token)
30
+ formatted_token = nil
31
+ transaction do
32
+ before_refresh_token
33
+ formatted_token = generate_refresh_token
34
+ remove_jwt_refresh_token_key(refresh_token)
35
+ after_refresh_token
36
+ end
37
+ json_response[jwt_refresh_token_key] = formatted_token
38
+ json_response[jwt_access_token_key] = session_jwt
39
+ else
40
+ json_response[json_response_error_key] = jwt_refresh_invalid_token_message
41
+ response.status ||= json_response_error_status
42
+ end
43
+ response['Content-Type'] ||= json_response_content_type
44
+ response.write(_json_response_body(json_response))
45
+ request.halt
46
+ end
47
+ end
48
+
49
+ def update_session
50
+ super
51
+
52
+ # JWT login puts the access token in the header.
53
+ # We put the refresh token in the body.
54
+ # Note, do not put the access_token in the body here, as the access token content is not yet finalised.
55
+ json_response['refresh_token'] = generate_refresh_token
56
+ end
57
+
58
+ def set_jwt_token(token)
59
+ super
60
+ if json_response[json_response_error_key]
61
+ json_response.delete(jwt_access_token_key)
62
+ else
63
+ json_response[jwt_access_token_key] = token
64
+ end
65
+ end
66
+
67
+ def jwt_session_hash
68
+ h = super
69
+ t = Time.now.to_i
70
+ h[:exp] = t + jwt_access_token_period
71
+ h[:iat] = t
72
+ h[:nbf] = t - jwt_access_token_not_before_period
73
+ h
74
+ end
75
+
76
+ def account_from_refresh_token(token)
77
+ @account = _account_from_refresh_token(token)
78
+ end
79
+
80
+ private
81
+
82
+ def _account_from_refresh_token(token)
83
+ id, token = split_token(token)
84
+ return unless id && token
85
+
86
+ token_id, key = split_token(token)
87
+ return unless token_id && key
88
+
89
+ return unless actual = get_active_refresh_token(id, token_id)
90
+
91
+ return unless timing_safe_eql?(key, convert_token_key(actual))
92
+
93
+ ds = account_ds(id)
94
+ ds = ds.where(account_status_column=>account_open_status_value) unless skip_status_checks?
95
+ ds.first
96
+ end
97
+
98
+ def get_active_refresh_token(account_id, token_id)
99
+ jwt_refresh_token_account_ds(account_id).
100
+ where(Sequel::CURRENT_TIMESTAMP > jwt_refresh_token_deadline_column).
101
+ delete
102
+
103
+ jwt_refresh_token_account_token_ds(account_id, token_id).
104
+ get(jwt_refresh_token_key_column)
105
+ end
106
+
107
+ def jwt_refresh_token_account_ds(account_id)
108
+ jwt_refresh_token_ds.where(jwt_refresh_token_account_id_column => account_id)
109
+ end
110
+
111
+ def jwt_refresh_token_account_token_ds(account_id, token_id)
112
+ jwt_refresh_token_account_ds(account_id).
113
+ where(jwt_refresh_token_id_column=>token_id)
114
+ end
115
+
116
+ def jwt_refresh_token_ds
117
+ db[jwt_refresh_token_table]
118
+ end
119
+
120
+ def remove_jwt_refresh_token_key(token)
121
+ account_id, token = split_token(token)
122
+ token_id, _ = split_token(token)
123
+ jwt_refresh_token_account_token_ds(account_id, token_id).delete
124
+ end
125
+
126
+ def generate_refresh_token
127
+ hash = jwt_refresh_token_insert_hash
128
+ [account_id, jwt_refresh_token_ds.insert(hash), convert_token_key(hash[jwt_refresh_token_key_column])].join(token_separator)
129
+ end
130
+
131
+ def jwt_refresh_token_insert_hash
132
+ hash = {jwt_refresh_token_account_id_column => account_id, jwt_refresh_token_key_column => random_key}
133
+ set_deadline_value(hash, jwt_refresh_token_deadline_column, jwt_refresh_token_deadline_interval)
134
+ hash
135
+ end
136
+
137
+ def after_close_account
138
+ jwt_refresh_token_account_ds(account_id).delete
139
+ super if defined?(super)
140
+ end
141
+ end
142
+ end
@@ -4,6 +4,8 @@ module Rodauth
4
4
  Feature.define(:lockout, :Lockout) do
5
5
  depends :login, :email_base
6
6
 
7
+ def_deprecated_alias :no_matching_unlock_account_key_error_flash, :no_matching_unlock_account_key_message
8
+
7
9
  loaded_templates %w'unlock-account-request unlock-account password-field unlock-account-email'
8
10
  view 'unlock-account-request', 'Request Account Unlock', 'unlock_account_request'
9
11
  view 'unlock-account', 'Unlock Account', 'unlock_account'
@@ -19,6 +21,7 @@ module Rodauth
19
21
  error_flash "There was an error unlocking your account", 'unlock_account'
20
22
  error_flash "This account is currently locked out and cannot be logged in to.", "login_lockout"
21
23
  error_flash "An email has recently been sent to you with a link to unlock the account", 'unlock_account_email_recently_sent'
24
+ error_flash "There was an error unlocking your account: invalid or expired unlock account key", 'no_matching_unlock_account_key'
22
25
  notice_flash "Your account has been unlocked", 'unlock_account'
23
26
  notice_flash "An email has been sent to you with a link to unlock your account", 'unlock_account_request'
24
27
  redirect :unlock_account
@@ -36,8 +39,9 @@ module Rodauth
36
39
  auth_value_method :account_lockouts_email_last_sent_column, nil
37
40
  auth_value_method :account_lockouts_deadline_column, :deadline
38
41
  auth_value_method :account_lockouts_deadline_interval, {:days=>1}
39
- auth_value_method :no_matching_unlock_account_key_message, 'No matching unlock account key'
40
42
  auth_value_method :unlock_account_email_subject, 'Unlock Account'
43
+ auth_value_method :unlock_account_explanatory_text, '<p>This account is currently locked out. You can unlock the account:</p>'
44
+ auth_value_method :unlock_account_request_explanatory_text, '<p>This account is currently locked out. You can request that the account be unlocked:</p>'
41
45
  auth_value_method :unlock_account_key_param, 'key'
42
46
  auth_value_method :unlock_account_requires_password?, false
43
47
  auth_value_method :unlock_account_skip_resend_email_within, 300
@@ -81,7 +85,7 @@ module Rodauth
81
85
  set_notice_flash unlock_account_request_notice_flash
82
86
  else
83
87
  set_redirect_error_status(no_matching_login_error_status)
84
- set_redirect_error_flash no_matching_login_message
88
+ set_redirect_error_flash no_matching_login_message.to_s.capitalize
85
89
  end
86
90
 
87
91
  redirect unlock_account_request_redirect
@@ -103,7 +107,7 @@ module Rodauth
103
107
  unlock_account_view
104
108
  else
105
109
  session[unlock_account_session_key] = nil
106
- set_redirect_error_flash no_matching_unlock_account_key_message
110
+ set_redirect_error_flash no_matching_unlock_account_key_error_flash
107
111
  redirect require_login_redirect
108
112
  end
109
113
  end
@@ -113,7 +117,7 @@ module Rodauth
113
117
  key = session[unlock_account_session_key] || param(unlock_account_key_param)
114
118
  unless account_from_unlock_key(key)
115
119
  set_redirect_error_status invalid_key_error_status
116
- set_redirect_error_flash no_matching_unlock_account_key_message
120
+ set_redirect_error_flash no_matching_unlock_account_key_error_flash
117
121
  redirect unlock_account_request_redirect
118
122
  end
119
123
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:login_password_requirements_base, :LoginPasswordRequirementsBase) do
5
+ auth_value_method :already_an_account_with_this_login_message, 'already an account with this login'
5
6
  auth_value_method :login_confirm_param, 'login-confirm'
6
7
  auth_value_method :login_minimum_length, 3
7
8
  auth_value_method :login_maximum_length, 255
@@ -61,7 +61,10 @@ module Rodauth
61
61
  auth_value_method :otp_keys_failures_column, :num_failures
62
62
  auth_value_method :otp_keys_table, :account_otp_keys
63
63
  auth_value_method :otp_keys_last_use_column, :last_use
64
+ auth_value_method :otp_provisioning_uri_label, 'Provisioning URL'
65
+ auth_value_method :otp_secret_label, 'Secret'
64
66
  auth_value_method :otp_setup_param, 'otp_secret'
67
+ auth_value_method :otp_setup_raw_param, 'otp_raw_secret'
65
68
 
66
69
  auth_cached_method :otp_key
67
70
  auth_cached_method :otp
@@ -71,7 +74,8 @@ module Rodauth
71
74
  :otp_auth_form_footer,
72
75
  :otp_issuer,
73
76
  :otp_lockout_error_flash,
74
- :otp_lockout_redirect
77
+ :otp_lockout_redirect,
78
+ :otp_keys_use_hmac?
75
79
  )
76
80
 
77
81
  auth_methods(
@@ -148,9 +152,15 @@ module Rodauth
148
152
  secret = param(otp_setup_param)
149
153
  catch_error do
150
154
  unless otp_valid_key?(secret)
155
+ otp_tmp_key(otp_new_secret)
151
156
  throw_error_status(invalid_field_error_status, otp_setup_param, otp_invalid_secret_message)
152
157
  end
153
- otp_tmp_key(secret)
158
+
159
+ if otp_keys_use_hmac?
160
+ otp_tmp_key(param(otp_setup_raw_param))
161
+ else
162
+ otp_tmp_key(secret)
163
+ end
154
164
 
155
165
  unless two_factor_password_match?(param(password_param))
156
166
  throw_error_status(invalid_password_error_status, password_param, invalid_password_message)
@@ -257,7 +267,9 @@ module Rodauth
257
267
  if otp.respond_to?(:verify_with_drift)
258
268
  otp.verify_with_drift(ot_pass, drift)
259
269
  else
270
+ # :nocov:
260
271
  otp.verify(ot_pass, :drift_behind=>drift, :drift_ahead=>drift)
272
+ # :nocov:
261
273
  end
262
274
  else
263
275
  otp.verify(ot_pass)
@@ -308,6 +320,18 @@ module Rodauth
308
320
  RQRCode::QRCode.new(otp_provisioning_uri).as_svg(:module_size=>8)
309
321
  end
310
322
 
323
+ def otp_user_key
324
+ @otp_user_key ||= if otp_keys_use_hmac?
325
+ otp_hmac_secret(otp_key)
326
+ else
327
+ otp_key
328
+ end
329
+ end
330
+
331
+ def otp_keys_use_hmac?
332
+ !!hmac_secret
333
+ end
334
+
311
335
  private
312
336
 
313
337
  def clear_cached_otp
@@ -319,15 +343,47 @@ module Rodauth
319
343
  clear_cached_otp
320
344
  end
321
345
 
346
+ def otp_hmac_secret(key)
347
+ base32_encode(compute_raw_hmac(ROTP::Base32.decode(key)), key.bytesize)
348
+ end
349
+
322
350
  def otp_valid_key?(secret)
323
- secret =~ /\A([a-z2-7]{16}|[a-z2-7]{32})\z/
351
+ return false unless secret =~ /\A([a-z2-7]{16}|[a-z2-7]{32})\z/
352
+ if otp_keys_use_hmac?
353
+ timing_safe_eql?(otp_hmac_secret(param(otp_setup_raw_param)), secret)
354
+ else
355
+ true
356
+ end
324
357
  end
325
358
 
326
- def otp_new_secret
327
- ROTP::Base32.random_base32
359
+ if ROTP::Base32.respond_to?(:random_base32)
360
+ # :nocov:
361
+ def otp_new_secret
362
+ ROTP::Base32.random_base32
363
+ end
364
+ # :nocov:
365
+ else
366
+ def otp_new_secret
367
+ ROTP::Base32.random.downcase
368
+ end
369
+ end
370
+
371
+ if RUBY_VERSION < '1.9'
372
+ # :nocov:
373
+ def base32_encode(data, length)
374
+ chars = 'abcdefghijklmnopqrstuvwxyz234567'
375
+ length.times.map{|i|chars[data[i] % 32].chr}.join
376
+ end
377
+ # :nocov:
378
+ else
379
+ def base32_encode(data, length)
380
+ chars = 'abcdefghijklmnopqrstuvwxyz234567'
381
+ length.times.map{|i|chars[data[i].ord % 32]}.join
382
+ end
328
383
  end
329
384
 
330
385
  def _otp_tmp_key(secret)
386
+ @otp_user_key = nil
331
387
  @otp_key = secret
332
388
  end
333
389
 
@@ -338,11 +394,12 @@ module Rodauth
338
394
  end
339
395
 
340
396
  def _otp_key
397
+ @otp_user_key = nil
341
398
  otp_key_ds.get(otp_keys_column)
342
399
  end
343
400
 
344
401
  def _otp
345
- otp_class.new(otp_key, :issuer=>otp_issuer, :digits=>otp_digits, :interval=>otp_interval)
402
+ otp_class.new(otp_user_key, :issuer=>otp_issuer, :digits=>otp_digits, :interval=>otp_interval)
346
403
  end
347
404
 
348
405
  def otp_key_ds