Enhance rate limit for sending password reset email
This commit is contained in:
parent
bcd4b059d5
commit
fd5d8a06ce
|
|
@ -177,8 +177,18 @@ class AuthController extends Controller
|
|||
if (config('mail.driver') == "")
|
||||
return json(trans('auth.forgot.close'), 1);
|
||||
|
||||
if (Session::has('last_mail_time') && (time() - session('last_mail_time')) < 60)
|
||||
return json(trans('auth.forgot.frequent-mail'), 1);
|
||||
$rateLimit = 180;
|
||||
$lastMailCacheKey = sha1('last_mail_'.Utils::getClientIp());
|
||||
$remain = $rateLimit + Cache::get($lastMailCacheKey, 0) - time();
|
||||
|
||||
// Rate limit
|
||||
if ($remain > 0) {
|
||||
return json([
|
||||
'errno' => 2,
|
||||
'msg' => trans('auth.forgot.frequent-mail'),
|
||||
'remain' => $remain
|
||||
]);
|
||||
}
|
||||
|
||||
// Get user instance
|
||||
$user = $users->get($request->input('email'), 'email');
|
||||
|
|
@ -199,8 +209,6 @@ class AuthController extends Controller
|
|||
$m->from(config('mail.username'), $site_name);
|
||||
$m->to($request->input('email'))->subject(trans('auth.forgot.mail.title', ['sitename' => $site_name]));
|
||||
});
|
||||
|
||||
Log::info("[Password Reset] Mail has been sent to [{$request->input('email')}] with token [$token]");
|
||||
} catch (\Exception $e) {
|
||||
// Write the exception to log
|
||||
report($e);
|
||||
|
|
@ -208,7 +216,7 @@ class AuthController extends Controller
|
|||
return json(trans('auth.forgot.failed', ['msg' => $e->getMessage()]), 2);
|
||||
}
|
||||
|
||||
Session::put('last_mail_time', time());
|
||||
Cache::put($lastMailCacheKey, time(), 60);
|
||||
|
||||
return json(trans('auth.forgot.success'), 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,6 +232,8 @@ describe('tests for "register" module', () => {
|
|||
});
|
||||
});
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('tests for "forgot" module', () => {
|
||||
const modulePath = '../auth/forgot';
|
||||
|
||||
|
|
@ -263,10 +265,20 @@ describe('tests for "forgot" module', () => {
|
|||
<input id="email" />
|
||||
<div id="captcha-form"></div>
|
||||
<input id="captcha" />
|
||||
<button id="forgot-button"></button>
|
||||
<button id="forgot-button" data-remain="60"></button>
|
||||
`;
|
||||
|
||||
require(modulePath);
|
||||
require(modulePath)();
|
||||
expect(setInterval).toHaveBeenCalledTimes(1);
|
||||
jest.runTimersToTime(1000);
|
||||
|
||||
$('button').click();
|
||||
expect($('button').prop('disabled')).toBe(true);
|
||||
expect(fetch).not.toBeCalled();
|
||||
|
||||
jest.runTimersToTime(60000);
|
||||
expect($('button').html()).toBe('auth.send');
|
||||
expect($('button').prop('disabled')).toBe(false);
|
||||
|
||||
$('button').click();
|
||||
expect(trans).toBeCalledWith('auth.emptyEmail');
|
||||
|
|
@ -294,7 +306,7 @@ describe('tests for "forgot" module', () => {
|
|||
captcha: 'captcha'
|
||||
}
|
||||
}));
|
||||
expect($('button').html()).toBe('auth.send');
|
||||
expect($('button').html()).toEqual(expect.stringContaining('auth.send'));
|
||||
expect($('button').prop('disabled')).toBe(true);
|
||||
expect(showMsg).toBeCalledWith('success', 'success');
|
||||
|
||||
|
|
@ -310,6 +322,8 @@ describe('tests for "forgot" module', () => {
|
|||
});
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
describe('tests for "reset" module', () => {
|
||||
const modulePath = '../auth/reset';
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ $('#forgot-button').click(e => {
|
|||
}
|
||||
})(data, async () => {
|
||||
try {
|
||||
const { errno, msg } = await fetch({
|
||||
const { errno, msg, remain } = await fetch({
|
||||
type: 'POST',
|
||||
url: url('auth/forgot'),
|
||||
dataType: 'json',
|
||||
|
|
@ -32,20 +32,55 @@ $('#forgot-button').click(e => {
|
|||
beforeSend: () => {
|
||||
$('#forgot-button').html(
|
||||
'<i class="fa fa-spinner fa-spin"></i> ' + trans('auth.sending')
|
||||
).prop('disabled', 'disabled');
|
||||
).prop('disabled', true);
|
||||
}
|
||||
});
|
||||
|
||||
if (errno === 0) {
|
||||
showMsg(msg, 'success');
|
||||
$('#forgot-button').html(trans('auth.send')).prop('disabled', 'disabled');
|
||||
showRemainTimeIndicator(180);
|
||||
} else {
|
||||
showMsg(msg, 'warning');
|
||||
refreshCaptcha();
|
||||
$('#forgot-button').html(trans('auth.send')).prop('disabled', '');
|
||||
|
||||
if (remain) {
|
||||
showRemainTimeIndicator(remain);
|
||||
} else {
|
||||
$('#forgot-button').html(trans('auth.send')).prop('disabled', false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showAjaxError(error);
|
||||
$('#forgot-button').html(trans('auth.send')).prop('disabled', '');
|
||||
$('#forgot-button').html(trans('auth.send')).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function showRemainTimeIndicator(seconds, intervalID) {
|
||||
// Get remain time from elem data if not specified
|
||||
if (seconds === undefined) {
|
||||
seconds = $('#forgot-button').data('remain');
|
||||
}
|
||||
|
||||
if (seconds > 0) {
|
||||
$('#forgot-button').html(`${trans('auth.send')} (${seconds})`).prop('disabled', true);
|
||||
} else {
|
||||
$('#forgot-button').html(trans('auth.send')).prop('disabled', false);
|
||||
// Stop timer
|
||||
if (intervalID) clearInterval(intervalID);
|
||||
}
|
||||
|
||||
// Create timer for decreasing remain time by second
|
||||
if (! intervalID) {
|
||||
const intervalID = window.setInterval(function () {
|
||||
showRemainTimeIndicator(--seconds, intervalID);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Start timer
|
||||
$(document).ready(() => showRemainTimeIndicator());
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
module.exports = showRemainTimeIndicator;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ forgot:
|
|||
message: We will send you an E-mail to verify.
|
||||
login-link: I do remember it
|
||||
close: Password resetting is not available now.
|
||||
frequent-mail: You click the send button too fast. Wait for 60 secs, guy.
|
||||
frequent-mail: You click the send button too fast. Wait for some minutes, guy.
|
||||
unregistered: The email address is not registered.
|
||||
success: Mail sent, please check your inbox. The link will be expired in 1 hour.
|
||||
failed: Fail to send mail, detailed message :msg
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ forgot:
|
|||
message: 我们将会向您发送一封验证邮件
|
||||
login-link: 我又想起来了
|
||||
close: 本站已关闭重置密码功能
|
||||
frequent-mail: 你邮件发送得太频繁啦,过 60 秒后再点发送吧
|
||||
frequent-mail: 你邮件发送得太频繁啦,过会儿再点发送吧
|
||||
unregistered: 该邮箱尚未注册
|
||||
success: 邮件已发送,一小时内有效,请注意查收。
|
||||
failed: 邮件发送失败,详细信息::msg
|
||||
|
|
|
|||
|
|
@ -42,8 +42,11 @@
|
|||
<a href="{{ url('auth/login') }}" class="text-center">{{ trans('auth.forgot.login-link') }}</a>
|
||||
</div>
|
||||
<!-- /.col -->
|
||||
<?php $remain = 180 + Cache::get(sha1('last_mail_'.Utils::getClientIp()), 0) - time(); ?>
|
||||
<div class="col-xs-4">
|
||||
<button id="forgot-button" class="btn btn-primary btn-block btn-flat">{{ trans('auth.forgot.button') }}</button>
|
||||
<button id="forgot-button" class="btn btn-primary btn-block btn-flat" data-remain="{{ $remain }}">
|
||||
{{ trans('auth.forgot.button') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- /.col -->
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -425,16 +425,21 @@ class AuthControllerTest extends TestCase
|
|||
]);
|
||||
config(['mail.driver' => 'smtp']);
|
||||
|
||||
$lastMailCacheKey = sha1('last_mail_'.Utils::getClientIp());
|
||||
|
||||
// Should be forbidden if sending email frequently
|
||||
$this->withSession(['last_mail_time' => time()])->post('/auth/forgot', [
|
||||
$this->withCache([
|
||||
$lastMailCacheKey => time()
|
||||
])->post('/auth/forgot', [
|
||||
'captcha' => 'a'
|
||||
])->seeJson([
|
||||
'errno' => 1,
|
||||
'errno' => 2,
|
||||
'msg' => trans('auth.forgot.frequent-mail')
|
||||
]);
|
||||
$this->flushCache();
|
||||
$this->flushSession();
|
||||
|
||||
// Should return a warning if user is not existed
|
||||
$this->flushSession();
|
||||
$user = factory(User::class)->create();
|
||||
$this->withSession(['phrase' => 'a'])->post('/auth/forgot', [
|
||||
'email' => 'nope@nope.net',
|
||||
|
|
@ -485,7 +490,8 @@ class AuthControllerTest extends TestCase
|
|||
])->seeJson([
|
||||
'errno' => 0,
|
||||
'msg' => trans('auth.forgot.success')
|
||||
])->assertSessionHas('last_mail_time');
|
||||
])->assertCacheHas($lastMailCacheKey);
|
||||
$this->flushCache();
|
||||
|
||||
// Should handle exception when sending email
|
||||
Mail::shouldReceive('send')
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user