rodauth 1.19.1 → 1.20.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|