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.
- checksums.yaml +4 -4
- data/CHANGELOG +72 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +100 -7
- data/doc/base.rdoc +25 -0
- data/doc/email_auth.rdoc +1 -1
- data/doc/email_base.rdoc +5 -1
- data/doc/internals.rdoc +2 -2
- data/doc/jwt_refresh.rdoc +35 -0
- data/doc/lockout.rdoc +3 -0
- data/doc/login_password_requirements_base.rdoc +4 -1
- data/doc/otp.rdoc +22 -39
- data/doc/recovery_codes.rdoc +15 -28
- data/doc/release_notes/1.20.0.txt +175 -0
- data/doc/remember.rdoc +3 -0
- data/doc/reset_password.rdoc +2 -1
- data/doc/single_session.rdoc +3 -0
- data/doc/verify_account.rdoc +4 -3
- data/doc/verify_login_change.rdoc +1 -1
- data/lib/rodauth.rb +33 -4
- data/lib/rodauth/features/base.rb +93 -10
- data/lib/rodauth/features/change_login.rb +1 -1
- data/lib/rodauth/features/confirm_password.rb +1 -1
- data/lib/rodauth/features/create_account.rb +2 -2
- data/lib/rodauth/features/disallow_password_reuse.rb +5 -3
- data/lib/rodauth/features/email_auth.rb +4 -2
- data/lib/rodauth/features/email_base.rb +12 -6
- data/lib/rodauth/features/jwt.rb +9 -0
- data/lib/rodauth/features/jwt_refresh.rb +142 -0
- data/lib/rodauth/features/lockout.rb +8 -4
- data/lib/rodauth/features/login_password_requirements_base.rb +1 -0
- data/lib/rodauth/features/otp.rb +63 -6
- data/lib/rodauth/features/recovery_codes.rb +1 -0
- data/lib/rodauth/features/remember.rb +20 -2
- data/lib/rodauth/features/reset_password.rb +5 -2
- data/lib/rodauth/features/single_session.rb +15 -2
- data/lib/rodauth/features/verify_account.rb +11 -6
- data/lib/rodauth/features/verify_login_change.rb +5 -3
- data/lib/rodauth/version.rb +2 -2
- data/spec/disallow_password_reuse_spec.rb +115 -28
- data/spec/email_auth_spec.rb +2 -2
- data/spec/jwt_refresh_spec.rb +256 -0
- data/spec/lockout_spec.rb +4 -4
- data/spec/login_spec.rb +52 -11
- data/spec/migrate/001_tables.rb +10 -0
- data/spec/migrate_travis/001_tables.rb +8 -0
- data/spec/remember_spec.rb +27 -0
- data/spec/reset_password_spec.rb +2 -2
- data/spec/rodauth_spec.rb +25 -1
- data/spec/single_session_spec.rb +20 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/two_factor_spec.rb +57 -3
- data/spec/verify_account_spec.rb +18 -1
- data/spec/verify_login_change_spec.rb +2 -2
- data/templates/add-recovery-codes.str +1 -1
- data/templates/change-password.str +2 -2
- data/templates/login-confirm-field.str +2 -2
- data/templates/login-field.str +2 -2
- data/templates/otp-auth-code-field.str +2 -2
- data/templates/otp-setup.str +4 -3
- data/templates/password-confirm-field.str +2 -2
- data/templates/password-field.str +2 -2
- data/templates/recovery-auth.str +2 -2
- data/templates/reset-password-request.str +1 -1
- data/templates/sms-code-field.str +2 -2
- data/templates/sms-setup.str +2 -2
- data/templates/unlock-account-request.str +1 -1
- data/templates/unlock-account.str +1 -1
- data/templates/verify-account-resend.str +1 -1
- 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 =
|
84
|
+
@login_requirement_message = already_an_account_with_this_login_message
|
85
85
|
end
|
86
86
|
updated && !raised
|
87
87
|
end
|
@@ -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 =
|
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
|
-
|
32
|
-
|
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
|
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 :
|
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
|
49
|
-
|
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
|
53
|
-
|
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
|
-
|
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?
|
data/lib/rodauth/features/jwt.rb
CHANGED
@@ -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
|
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
|
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
|
data/lib/rodauth/features/otp.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
327
|
-
|
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(
|
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
|