1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
|
# frozen-string-literal: true
module Rodauth
Feature.define(:otp_unlock, :OtpUnlock) do
depends :otp
before 'otp_unlock_attempt'
after 'otp_unlock_auth_success'
after 'otp_unlock_auth_failure'
after 'otp_unlock_not_yet_available'
error_flash "TOTP authentication is not currently locked out", 'otp_unlock_not_locked_out'
error_flash "TOTP invalid authentication", 'otp_unlock_auth_failure'
error_flash "Deadline past for unlocking TOTP authentication", 'otp_unlock_auth_deadline_passed'
error_flash "TOTP unlock attempt not yet available", 'otp_unlock_auth_not_yet_available'
notice_flash "TOTP authentication unlocked", 'otp_unlocked'
notice_flash "TOTP successful authentication, more successful authentication needed to unlock", 'otp_unlock_auth_success'
redirect :otp_unlock_not_locked_out
redirect :otp_unlocked
additional_form_tags
button 'Authenticate Using TOTP to Unlock', 'otp_unlock'
auth_value_method :otp_unlock_auth_deadline_passed_error_status, 403
auth_value_method :otp_unlock_auth_failure_cooldown_seconds, 900
auth_value_method :otp_unlock_auth_failure_error_status, 403
auth_value_method :otp_unlock_auth_not_yet_available_error_status, 403
auth_value_method :otp_unlock_auths_required, 3
auth_value_method :otp_unlock_deadline_seconds, 900
auth_value_method :otp_unlock_id_column, :id
auth_value_method :otp_unlock_next_auth_attempt_after_column, :next_auth_attempt_after
auth_value_method :otp_unlock_not_locked_out_error_status, 403
auth_value_method :otp_unlock_num_successes_column, :num_successes
auth_value_method :otp_unlock_table, :account_otp_unlocks
translatable_method :otp_unlock_consecutive_successes_label, 'Consecutive successful authentications'
translatable_method :otp_unlock_form_footer, ''
translatable_method :otp_unlock_next_auth_attempt_label, 'Can attempt next authentication after'
translatable_method :otp_unlock_next_auth_attempt_refresh_label, 'Page will automatically refresh when authentication is possible.'
translatable_method :otp_unlock_next_auth_deadline_label, 'Deadline for next authentication'
translatable_method :otp_unlock_required_consecutive_successes_label, 'Required consecutive successful authentications to unlock'
loaded_templates %w'otp-unlock otp-unlock-not-available'
view 'otp-unlock', 'Unlock TOTP Authentication', 'otp_unlock'
view 'otp-unlock-not-available', 'Must Wait to Unlock TOTP Authentication', 'otp_unlock_not_available'
auth_methods(
:otp_unlock_auth_failure,
:otp_unlock_auth_success,
:otp_unlock_available?,
:otp_unlock_deadline_passed?,
:otp_unlock_not_available_set_refresh_header,
:otp_unlock_refresh_tag,
)
route(:otp_unlock) do |r|
require_login
require_account_session
require_otp_setup
unless otp_locked_out?
set_response_error_reason_status(:otp_not_locked_out, otp_unlock_not_locked_out_error_status)
set_redirect_error_flash otp_unlock_not_locked_out_error_flash
redirect otp_unlock_not_locked_out_redirect
end
before_otp_unlock_route
r.get do
if otp_unlock_available?
otp_unlock_view
else
otp_unlock_not_available_set_refresh_header
otp_unlock_not_available_view
end
end
r.post do
db.transaction do
if otp_unlock_deadline_passed?
set_response_error_reason_status(:otp_unlock_deadline_passed, otp_unlock_auth_deadline_passed_error_status)
set_redirect_error_flash otp_unlock_auth_deadline_passed_error_flash
elsif !otp_unlock_available?
after_otp_unlock_not_yet_available
set_response_error_reason_status(:otp_unlock_not_yet_available, otp_unlock_auth_not_yet_available_error_status)
set_redirect_error_flash otp_unlock_auth_not_yet_available_error_flash
else
before_otp_unlock_attempt
if otp_valid_code?(param(otp_auth_param))
otp_unlock_auth_success
after_otp_unlock_auth_success
unless otp_locked_out?
set_notice_flash otp_unlocked_notice_flash
redirect otp_unlocked_redirect
end
set_notice_flash otp_unlock_auth_success_notice_flash
else
otp_unlock_auth_failure
after_otp_unlock_auth_failure
set_response_error_reason_status(:otp_unlock_auth_failure, otp_unlock_auth_failure_error_status)
set_redirect_error_flash otp_unlock_auth_failure_error_flash
end
end
end
redirect request.path
end
end
def otp_unlock_available?
if otp_unlock_data
next_auth_attempt_after = otp_unlock_next_auth_attempt_after
current_timestamp = Time.now
if (next_auth_attempt_after < current_timestamp - otp_unlock_deadline_seconds)
# Unlock process not fully completed within deadline, reset process
otp_unlock_reset
true
else
if next_auth_attempt_after > current_timestamp
# If next auth attempt after timestamp is in the future, that means the next
# unlock attempt cannot happen until then.
false
else
if otp_unlock_num_successes == 0
# 0 value indicates previous attempt was a failure. Since failure cooldown
# period has passed, reset process so user gets full deadline period
otp_unlock_reset
end
true
end
end
else
# No row means no unlock attempts yet (or previous attempt was more than the
# deadline account, so unlocking is available
true
end
end
def otp_unlock_auth_failure
h = {
otp_unlock_num_successes_column=>0,
otp_unlock_next_auth_attempt_after_column=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, :seconds=>otp_unlock_auth_failure_cooldown_seconds)
}
if otp_unlock_ds.update(h) == 0
h[otp_unlock_id_column] = session_value
# If row already exists when inserting, no need to do anything
raises_uniqueness_violation?{otp_unlock_ds.insert(h)}
end
end
def otp_unlock_auth_success
deadline = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, :seconds=>otp_unlock_success_cooldown_seconds)
# Add WHERE to avoid possible race condition when multiple unlock auth requests
# are sent at the same time (only the first should increment num successes).
if otp_unlock_ds.
where(Sequel[otp_unlock_next_auth_attempt_after_column] < Sequel::CURRENT_TIMESTAMP).
update(
otp_unlock_num_successes_column=>Sequel[otp_unlock_num_successes_column]+1,
otp_unlock_next_auth_attempt_after_column=>deadline
) == 0
# Ignore uniqueness errors when inserting after a failed update,
# which could be caused due to the race condition mentioned above.
raises_uniqueness_violation? do
otp_unlock_ds.insert(
otp_unlock_id_column=>session_value,
otp_unlock_next_auth_attempt_after_column=>deadline
)
end
end
@otp_unlock_data = nil
# :nocov:
if otp_unlock_data
# :nocov:
if otp_unlock_num_successes >= otp_unlock_auths_required
# At least the requisite number of consecutive successful unlock
# authentications. Unlock OTP authentication.
otp_key_ds.update(otp_keys_failures_column => 0)
# Remove OTP unlock metadata when unlocking OTP authentication
otp_unlock_reset
# else
# # Still need additional consecutive successful unlock attempts.
end
# else
# # if row isn't available, probably the process was reset during this,
# # and it's safe to do nothing in that case.
end
end
def otp_unlock_deadline_passed?
otp_unlock_data ? (otp_unlock_next_auth_attempt_after < Time.now - otp_unlock_deadline_seconds) : false
end
def otp_unlock_refresh_tag
# RODAUTH3: Remove
"<meta http-equiv=\"refresh\" content=\"#{(otp_unlock_next_auth_attempt_after - Time.now).to_i + 1}\">"
end
def otp_lockout_redirect
otp_unlock_path
end
def otp_unlock_next_auth_attempt_after
if otp_unlock_data
convert_timestamp(otp_unlock_data[otp_unlock_next_auth_attempt_after_column])
else
Time.now
end
end
def otp_unlock_deadline
otp_unlock_next_auth_attempt_after + otp_unlock_deadline_seconds
end
def otp_unlock_num_successes
otp_unlock_data ? otp_unlock_data[otp_unlock_num_successes_column] : 0
end
def otp_unlock_not_available_set_refresh_header
response.headers["refresh"] = ((otp_unlock_next_auth_attempt_after - Time.now).to_i + 1).to_s
end
private
def show_otp_auth_link?
super || (otp_exists? && otp_locked_out?)
end
def otp_unlock_data
@otp_unlock_data ||= otp_unlock_ds.first
end
def otp_unlock_success_cooldown_seconds
(_otp_interval+(otp_drift||0))*2
end
def otp_unlock_reset
otp_unlock_ds.delete
@otp_unlock_data = nil
end
def otp_unlock_ds
db[otp_unlock_table].where(otp_unlock_id_column=>session_value)
end
end
end
|