Improving user locked out logic.
This patch improves on the user locked out logic by providing a helping message near locked out. This would help reduce retries by giving user the option to use password reset flow to fix the issue. PROD-1505
This commit is contained in:
@@ -970,6 +970,16 @@ class LoginFailures(models.Model):
|
||||
|
||||
record.save()
|
||||
|
||||
@classmethod
|
||||
def check_user_reset_password_threshold(cls, user):
|
||||
"""
|
||||
Checks if the user is above threshold for reset password message.
|
||||
"""
|
||||
record, _ = LoginFailures.objects.get_or_create(user=user)
|
||||
max_failures_allowed = settings.MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED
|
||||
|
||||
return record.failure_count >= max_failures_allowed / 2, record.failure_count
|
||||
|
||||
@classmethod
|
||||
def clear_lockout_counter(cls, user):
|
||||
"""
|
||||
|
||||
@@ -193,11 +193,11 @@
|
||||
expect($('.button-oa2-facebook')).toBeVisible();
|
||||
});
|
||||
|
||||
it('displays a link to the password reset form', function() {
|
||||
it('displays a link to the signin help', function() {
|
||||
createLoginView(this);
|
||||
|
||||
// Verify that the password reset link is displayed
|
||||
expect($('.forgot-password')).toBeVisible();
|
||||
// Verify that the Signin help link is displayed
|
||||
expect($('.login-help')).toBeVisible();
|
||||
});
|
||||
|
||||
it('displays a link to the enterprise slug login', function() {
|
||||
|
||||
@@ -105,6 +105,13 @@
|
||||
expect($(view.el).html().length).toEqual(0);
|
||||
});
|
||||
|
||||
it('displays a link to the password reset help', function() {
|
||||
createPasswordResetView(this);
|
||||
|
||||
// Verify that the password reset help link is displayed
|
||||
expect($('.reset-help')).toBeVisible();
|
||||
});
|
||||
|
||||
it('validates the email field', function() {
|
||||
createPasswordResetView(this);
|
||||
|
||||
|
||||
@@ -264,6 +264,12 @@
|
||||
|
||||
// Load the form. Institution login is always refreshed since it changes based on the previous form.
|
||||
if (!this.form.isLoaded($form) || type == 'institution_login') {
|
||||
|
||||
// We need a special case for loading reset form as there is mismatch of form id
|
||||
// value ie 'password-reset' vs load function name ie 'reset'
|
||||
if (type === 'password-reset') {
|
||||
type = 'reset';
|
||||
}
|
||||
this.loadForm(type);
|
||||
}
|
||||
this.activeForm = type;
|
||||
|
||||
@@ -144,12 +144,16 @@
|
||||
$form = this.$form,
|
||||
elements = $form[0].elements,
|
||||
i,
|
||||
$n,
|
||||
tpl,
|
||||
len = elements.length,
|
||||
$el,
|
||||
$label,
|
||||
key = '',
|
||||
errors = [],
|
||||
validation = {};
|
||||
validation = {},
|
||||
$desc,
|
||||
$validationNode;
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
$el = $(elements[i]);
|
||||
@@ -163,12 +167,29 @@
|
||||
}
|
||||
|
||||
if (key) {
|
||||
if (this.interesting_fields($el)) {
|
||||
this.remove_validation_error($el, $form);
|
||||
}
|
||||
validation = this.validate(elements[i]);
|
||||
if (validation.isValid) {
|
||||
obj[key] = $el.attr('type') === 'checkbox' ? $el.is(':checked') : $el.val();
|
||||
$el.removeClass('error');
|
||||
$label.removeClass('error');
|
||||
} else {
|
||||
if (this.interesting_fields($el)) {
|
||||
$validationNode = this.get_error_validation_node($el, $form);
|
||||
if ($validationNode) {
|
||||
$n = $.parseHTML(validation.message);
|
||||
tpl = HtmlUtils.template('<i class="fa fa-exclamation-triangle"></i>');
|
||||
|
||||
HtmlUtils.prepend($n, tpl());
|
||||
HtmlUtils.append($validationNode, HtmlUtils.HTML($n));
|
||||
}
|
||||
|
||||
$desc = $form.find('#' + $el.attr('id') + '-desc');
|
||||
$desc.remove();
|
||||
}
|
||||
|
||||
errors.push(validation.message);
|
||||
$el.addClass('error');
|
||||
$label.addClass('error');
|
||||
@@ -180,6 +201,34 @@
|
||||
|
||||
return obj;
|
||||
},
|
||||
remove_validation_error: function($el, $form) {
|
||||
var $validationNode = this.get_error_validation_node($el, $form);
|
||||
if ($validationNode && $validationNode.find('li').length > 0) {
|
||||
$validationNode.empty();
|
||||
}
|
||||
},
|
||||
|
||||
get_error_validation_node: function($el, $form) {
|
||||
var $node = $form.find('#' + $el.attr('id') + '-validation-error-msg');
|
||||
return $node.find('ul');
|
||||
},
|
||||
|
||||
interesting_fields: function($el) {
|
||||
return ($el.attr('name') === 'email' || $el.attr('name') === 'password');
|
||||
},
|
||||
|
||||
toggleHelp: function(event, $help) {
|
||||
var $el = $(event.currentTarget);
|
||||
var $i = $el.find('i');
|
||||
|
||||
if ($help.css('display') === 'block') {
|
||||
$help.css('display', 'none');
|
||||
$i.addClass('fa-caret-right').removeClass('fa-caret-down');
|
||||
} else {
|
||||
$help.css('display', 'block');
|
||||
$i.addClass('fa-caret-down').removeClass('fa-caret-right');
|
||||
}
|
||||
},
|
||||
|
||||
saveError: function(error) {
|
||||
this.errors = [
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
'click .js-login': 'submitForm',
|
||||
'click .forgot-password': 'forgotPassword',
|
||||
'click .login-provider': 'thirdPartyAuth',
|
||||
'click .enterprise-login': 'enterpriseSlugLogin'
|
||||
'click .enterprise-login': 'enterpriseSlugLogin',
|
||||
'click .login-help': 'toggleLoginHelp'
|
||||
},
|
||||
formType: 'login',
|
||||
requiredStr: '',
|
||||
@@ -139,6 +140,13 @@
|
||||
this.clearPasswordResetSuccess();
|
||||
},
|
||||
|
||||
toggleLoginHelp: function(event) {
|
||||
var $help;
|
||||
event.preventDefault();
|
||||
$help = $('#login-help');
|
||||
this.toggleHelp(event, $help);
|
||||
},
|
||||
|
||||
enterpriseSlugLogin: function(event) {
|
||||
event.preventDefault();
|
||||
if (this.enterpriseSlugLoginURL) {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
tpl: '#password_reset-tpl',
|
||||
|
||||
events: {
|
||||
'click .js-reset': 'submitForm'
|
||||
'click .js-reset': 'submitForm',
|
||||
'click .reset-help': 'toggleResetHelp'
|
||||
},
|
||||
|
||||
formType: 'password-reset',
|
||||
@@ -27,6 +28,13 @@
|
||||
this.listenTo(this.model, 'sync', this.saveSuccess);
|
||||
},
|
||||
|
||||
toggleResetHelp: function(event) {
|
||||
var $help;
|
||||
event.preventDefault();
|
||||
$help = $('#reset-help');
|
||||
this.toggleHelp(event, $help);
|
||||
},
|
||||
|
||||
saveSuccess: function() {
|
||||
this.trigger('password-email-sent');
|
||||
|
||||
|
||||
@@ -98,6 +98,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
#login-help, #reset-help {
|
||||
padding-left: 8px;
|
||||
}
|
||||
ul.fa-ul{
|
||||
margin: 0 0 0 0;
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-register {
|
||||
$grid-columns: 12;
|
||||
|
||||
@@ -373,8 +383,8 @@
|
||||
@extend %t-copy-sub2;
|
||||
|
||||
display: block;
|
||||
margin-bottom: ($baseline/2);
|
||||
margin-top: ($baseline/4);
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
@@ -391,6 +401,11 @@
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
> i {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0 2px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
|
||||
@@ -127,13 +127,24 @@
|
||||
|
||||
<span id="<%- form %>-<%- name %>-validation-error" class="tip error" aria-live="assertive">
|
||||
<span class="sr-only"></span>
|
||||
<span id="<%- form %>-<%- name %>-validation-error-msg"></span>
|
||||
<span id="<%- form %>-<%- name %>-validation-error-msg"><ul class="fa-ul"></ul></span>
|
||||
</span>
|
||||
<% if ( instructions ) { %> <span class="tip tip-input" id="<%- form %>-<%- name %>-desc"><%- instructions %></span><% } %>
|
||||
<% } %>
|
||||
|
||||
<% if( form === 'login' && name === 'password' ) { %>
|
||||
<button type="button" class="forgot-password field-link"><%- gettext("Need help logging in?") %></button>
|
||||
<button type="button" class="login-help field-link"><i class="fa fa-caret-right" /><%- gettext("Need help signing in?") %></button>
|
||||
<div id="login-help" style="display:none">
|
||||
<button type="button" class="field-link form-toggle" data-type="password-reset"><%- gettext("Forgot my password") %></button>
|
||||
<span><a class="field-link" href="https://support.edx.org/hc/en-us/sections/115004153367-Solve-a-Sign-in-Problem"><%- gettext("Other sign-in issues") %></a></span>
|
||||
</div>
|
||||
<button type="button" class="enterprise-login field-link"><%- gettext("Sign in with your company or school") %></button>
|
||||
<% } %>
|
||||
<% if( form === 'password-reset' && name === 'email' ) { %>
|
||||
<button type="button" class="reset-help field-link" ><i class="fa fa-caret-right" /><%- gettext("Need other help signing in?") %></button>
|
||||
<div id="reset-help" style="display:none">
|
||||
<button type="button" class="field-link form-toggle" data-type="register"><%- gettext("Create an account") %></button>
|
||||
<span><a class="field-link" href="https://support.edx.org/hc/en-us/sections/115004153367-Solve-a-Sign-in-Problem"><%- gettext("Other sign-in issues") %></a></span>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
@@ -113,8 +113,21 @@ def _check_excessive_login_attempts(user):
|
||||
"""
|
||||
if user and LoginFailures.is_feature_enabled():
|
||||
if LoginFailures.is_user_locked_out(user):
|
||||
raise AuthFailedError(_('This account has been temporarily locked due '
|
||||
'to excessive login failures. Try again later.'))
|
||||
_generate_locked_out_error_message()
|
||||
|
||||
|
||||
def _generate_locked_out_error_message():
|
||||
|
||||
locked_out_period_in_sec = settings.MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS
|
||||
raise AuthFailedError(Text(_('To protect your account, it’s been temporarily '
|
||||
'locked. Try again in {locked_out_period} minutes.'
|
||||
'{li_start}To be on the safe side, you can reset your '
|
||||
'password {link_start}here{link_end} before you try again.')).format(
|
||||
link_start=HTML('<a "#login" class="form-toggle" data-type="password-reset">'),
|
||||
link_end=HTML('</a>'),
|
||||
li_start=HTML('<li>'),
|
||||
li_end=HTML('</li>'),
|
||||
locked_out_period=int(locked_out_period_in_sec / 60)))
|
||||
|
||||
|
||||
def _enforce_password_policy_compliance(request, user):
|
||||
@@ -230,6 +243,27 @@ def _handle_failed_authentication(user, authenticated_user):
|
||||
else:
|
||||
AUDIT_LOG.warning(u"Login failed - password for {0} is invalid".format(user.email))
|
||||
|
||||
if user and LoginFailures.is_feature_enabled():
|
||||
blocked_threshold, failure_count = LoginFailures.check_user_reset_password_threshold(user)
|
||||
if blocked_threshold:
|
||||
if not LoginFailures.is_user_locked_out(user):
|
||||
max_failures_allowed = settings.MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED
|
||||
remaining_attempts = max_failures_allowed - failure_count
|
||||
raise AuthFailedError(Text(_('Email or password is incorrect.'
|
||||
'{li_start}You have {remaining_attempts} more sign-in '
|
||||
'attempts before your account is temporarily locked.{li_end}'
|
||||
'{li_start}If you\'ve forgotten your password, click '
|
||||
'{link_start}here{link_end} to reset.{li_end}'
|
||||
))
|
||||
.format(
|
||||
link_start=HTML('<a http="#login" class="form-toggle" data-type="password-reset">'),
|
||||
link_end=HTML('</a>'),
|
||||
li_start=HTML('<li>'),
|
||||
li_end=HTML('</li>'),
|
||||
remaining_attempts=remaining_attempts))
|
||||
else:
|
||||
_generate_locked_out_error_message()
|
||||
|
||||
raise AuthFailedError(_('Email or password is incorrect.'))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user