rodauth 1.19.1 → 1.20.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 (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