def _check_excessive_login_attempts(user): """ See if account has been locked out due to excessive login failures """ if user and LoginFailures.is_feature_enabled(): if LoginFailures.is_user_locked_out(user): _generate_locked_out_error_message()
def _handle_failed_authentication_viatris(user,site): """ Handles updating the failed login count, inactive user notifications, and logging failed authentications. """ if user: if 'viatris-via' not in site: if LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user) if not user.is_active: _log_and_raise_inactive_user_auth_error(user) # if we didn't find this username earlier, the account for this email # doesn't exist, and doesn't have a corresponding password if settings.FEATURES['SQUELCH_PII_IN_LOGS']: loggable_id = user.id if user else "<unknown>" AUDIT_LOG.warning(u"Login failed - password for user.id: {0} is invalid".format(loggable_id)) else: AUDIT_LOG.warning(u"Login failed - password for {0} is invalid".format(user.email)) if 'viatris-via' in site: raise AuthFailedError(_('Incorrect Password. You can reset your password using Forgot password link or Sign In using the OTP option.By entering you email address you will receive a OTP (One Time Password) by email that will be valid for 2 minutes and you can use it to sign-in into the VIA platform and join lectures.')) elif 'viatris-kreon' in site or 'viatris-farmaciaformacion' in site: raise AuthFailedError(_('Contrasena incorrecta. Puede restablecer su contrasena utilizando el enlace Olvide mi contrasena.')) elif 'viatris-pvp-i' in site or 'viatris.' in site or 'viatris-multimodal' in site: raise AuthFailedError(_('Incorrect Password. You can reset your password using Forgot password link.')) elif 'viatris-atpon' in site: raise AuthFailedError(_('Niepoprawne haslo. Mozesz zresetowac swoje haslo, korzystajac z linku Nie pamietam hasla.')) elif 'viatris-norge' in site: raise AuthFailedError(_('Du kan tilbakestille passordet ved a bruke Glemt passord - koblingen eller Logg pa med OTP-alternativet.Ved a skrive inn din e-postadresse vil du motta et OTP (engangspassord) pa e-post som vil vaere gyldig i 2 minutter, og som du kan bruke til a logge deg pa norge-plattformen og bli med pa forelesninger.')) else: raise AuthFailedError(_('Incorrect Password. You can reset your password using Forgot password link or Sign In using the OTP option.'))
def _handle_successful_authentication_and_login(user, request): """ Handles clearing the failed login counter, login tracking, and setting session timeout. """ if LoginFailures.is_feature_enabled(): LoginFailures.clear_lockout_counter(user) _track_user_login(user, request) try: django_login(request, user) request.session.set_expiry(604800 * 4) log.debug("Setting user session expiry to 4 weeks") # .. event_implemented_name: SESSION_LOGIN_COMPLETED SESSION_LOGIN_COMPLETED.send_event(user=UserData( pii=UserPersonalData( username=user.username, email=user.email, name=user.profile.name, ), id=user.id, is_active=user.is_active, ), ) except Exception as exc: AUDIT_LOG.critical( "Login failed - Could not create session. Is memcached running?") log.critical( "Login failed - Could not create session. Is memcached running?") log.exception(exc) raise
def test_password_reset_with_login_failures_feature_disabled(self): """ Tests that user's login failures lockout counter is not reset upon successful password reset. """ # Adding an entry in LoginFailures to verify the password reset endpoint # does not reset the user's login failures lockout counter. LoginFailures.increment_lockout_counter(self.user) request_params = { 'new_password1': 'password1', 'new_password2': 'password1' } confirm_request = self.request_factory.post( self.password_reset_confirm_url, data=request_params) self.setup_request_session_with_token(confirm_request) confirm_request.user = self.user # Make a password reset request. resp = PasswordResetConfirmWrapper.as_view()(confirm_request, uidb36=self.uidb36, token=self.token) # Verify that the user's login failures lockout count is not reset. assert resp.status_code == 302 assert not LoginFailures.is_feature_enabled() assert LoginFailures.is_user_locked_out(confirm_request.user)
def _enforce_password_policy_compliance(request, user): # lint-amnesty, pylint: disable=missing-function-docstring try: password_policy_compliance.enforce_compliance_on_login(user, request.POST.get('password')) except password_policy_compliance.NonCompliantPasswordWarning as e: # Allow login, but warn the user that they will be required to reset their password soon. PageLevelMessages.register_warning_message(request, str(e)) except password_policy_compliance.NonCompliantPasswordException as e: # Increment the lockout counter to safguard from further brute force requests # if user's password has been compromised. if LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user) AUDIT_LOG.info("Password reset initiated for email %s.", user.email) send_password_reset_email_for_user(user, request) # Prevent the login attempt. raise AuthFailedError(HTML(str(e)), error_code=e.__class__.__name__) # lint-amnesty, pylint: disable=raise-missing-from
def _handle_successful_authentication_and_login(user, request): """ Handles clearing the failed login counter, login tracking, and setting session timeout. """ if LoginFailures.is_feature_enabled(): LoginFailures.clear_lockout_counter(user) _track_user_login(user, request) try: django_login(request, user) request.session.set_expiry(604800 * 4) log.debug("Setting user session expiry to 4 weeks") except Exception as exc: AUDIT_LOG.critical("Login failed - Could not create session. Is memcached running?") log.critical("Login failed - Could not create session. Is memcached running?") log.exception(exc) raise
def test_password_reset_request_with_login_failures_feature_disabled(self): """ Tests that user's login failures lockout counter is not reset upon successful password reset. """ # Adding an entry in LoginFailures to verify the password reset endpoint # does not reset the user's login failures lockout counter. LoginFailures.increment_lockout_counter(self.user) post_request = self.create_reset_request(self.uidb36, self.token, False) post_request.user = AnonymousUser() reset_view = LogistrationPasswordResetView.as_view() reset_view(post_request, uidb36=self.uidb36, token=self.token).render() # Verify that the user's login failures lockout count is not reset. assert not LoginFailures.is_feature_enabled() assert LoginFailures.is_user_locked_out(self.user)
def post(self, request, *args, **kwargs): # We have to make a copy of request.POST because it is a QueryDict object which is immutable until copied. # We have to use request.POST because the password_reset_confirm method takes in the request and a user's # password is set to the request.POST['new_password1'] field. We have to also normalize the new_password2 # field so it passes the equivalence check that new_password1 == new_password2 # In order to switch out of having to do this copy, we would want to move the normalize_password code into # a custom User model's set_password method to ensure it is always happening upon calling set_password. request.POST = request.POST.copy() request.POST['new_password1'] = normalize_password( request.POST['new_password1']) request.POST['new_password2'] = normalize_password( request.POST['new_password2']) is_account_recovery = 'is_account_recovery' in request.GET password = request.POST['new_password1'] response = self._validate_password(password, request) if response: return response response = self._process_password_reset_success( request, self.token, self.uidb64, extra_context=self.platform_name) # If password reset was unsuccessful a template response is returned (status_code 200). # Check if form is invalid then show an error to the user. # Note if password reset was successful we get response redirect (status_code 302). password_reset_successful = response.status_code == 302 if not password_reset_successful: return self._handle_password_reset_failure(response) updated_user = User.objects.get(id=self.uid_int) if is_account_recovery: self._handle_primary_email_update(updated_user) updated_user.save() if password_reset_successful and is_account_recovery: self._handle_password_creation(request, updated_user) # Handles clearing the failed login counter upon password reset. if LoginFailures.is_feature_enabled(): LoginFailures.clear_lockout_counter(updated_user) update_session_auth_hash(request, updated_user) send_password_reset_success_email(updated_user, request) return response
def test_check_password_policy_compliance_exception(self): """ Tests _enforce_password_policy_compliance fails with an exception thrown """ assert not LoginFailures.objects.filter(user=self.user).exists() enforce_compliance_on_login = '******' with patch(enforce_compliance_on_login) as mock_enforce_compliance_on_login: mock_enforce_compliance_on_login.side_effect = NonCompliantPasswordException() response, _ = self._login_response( self.user_email, self.password ) response_content = json.loads(response.content.decode('utf-8')) assert not response_content.get('success') assert len(mail.outbox) == 1 assert 'Password reset' in mail.outbox[0].subject failure_record = LoginFailures.objects.get(user=self.user) assert failure_record.failure_count == 1 LoginFailures.clear_lockout_counter(user=self.user)
def _handle_failed_authentication_microsite(user): """ Handles updating the failed login count, inactive user notifications, and logging failed authentications. """ if user: if LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user) if not user.is_active: _log_and_raise_inactive_user_auth_error(user) # if we didn't find this username earlier, the account for this email # doesn't exist, and doesn't have a corresponding password if settings.FEATURES['SQUELCH_PII_IN_LOGS']: loggable_id = user.id if user else "<unknown>" AUDIT_LOG.warning(u"Login failed - password for user.id: {0} is invalid".format(loggable_id)) else: AUDIT_LOG.warning(u"Login failed - password for {0} is invalid".format(user.email)) raise AuthFailedError(_('Incorrect Password. You can reset your password using Forgot password link or Sign In using the OTP option.'))
def _check_excessive_login_attempts_viatris(user,site): """ See if account has been locked out due to excessive login failures """ #log.info('site--> %s', site) if 'viatris-via' not in site: if user and LoginFailures.is_feature_enabled(): if LoginFailures.is_user_locked_out(user): if 'viatris-via' in site: raise AuthFailedError(_('The account has been temporarily locked due to excessive login failures. Try again after 5 mins. or Sign In using the One Time Password(OTP) option.By entering you email address you will receive a OTP (One Time Password) by email that will be valid for 2 minutes and you can use it to sign-in into the VIA platform and join lectures.')) elif 'viatris-kreon' in site or 'viatris-farmaciaformacion' in site: #log.info('site3--> %s', site) raise AuthFailedError(_('La cuenta se ha bloqueado temporalmente debido a un numero excesivo de errores de inicio de sesion. Intentalo de nuevo despus de 5 minutos.')) elif 'viatris-pvp-i' in site or 'viatris-multimodal' in site: raise AuthFailedError(_('The account has been temporarily locked due to excessive login failures. Try again after 5 mins.')) elif 'viatris-atpon' in site: raise AuthFailedError(_('Konto zostalo tymczasowo zablokowane z powodu nadmiernych niepowodzen logowania. Sprobuj ponownie za 5 minut.')) elif 'viatris-norge' in site: raise AuthFailedError(_('Kontoen er midlertidig last pa grunn av store paloggingsfeil. Prov pa nytt etter 5 minutter.')) else: raise AuthFailedError(_('The account has been temporarily locked due to excessive login failures. Try again after 5 mins. or Sign In using the One Time Password(OTP) option.'))
def test_password_reset_request_with_login_failures_feature_enabled(self): """ Tests that user's login failures lockout counter is reset upon successful password reset. """ # Adding an entry in LoginFailures to verify the password reset endpoint # reset the user's login failures lockout counter. LoginFailures.increment_lockout_counter(self.user) post_request = self.create_reset_request(self.uidb36, self.token, False) post_request.user = AnonymousUser() reset_view = LogistrationPasswordResetView.as_view() json_response = reset_view(post_request, uidb36=self.uidb36, token=self.token).render() json_response = json.loads(json_response.content.decode('utf-8')) # Verify that the user's login failures lockout count is reset. assert json_response.get('reset_status') assert not LoginFailures.is_user_locked_out(self.user) # Verify that the user's login failures lockout counter is not reset upon # password reset failure. LoginFailures.increment_lockout_counter(self.user) post_request = self.create_reset_request(self.uidb36, self.token, False, 'new_password2') post_request.user = AnonymousUser() reset_view = LogistrationPasswordResetView.as_view() reset_view(post_request, uidb36=self.uidb36, token=self.token).render() assert LoginFailures.is_user_locked_out(self.user)
def _handle_inactive_user_failed_authentication(user): """ Handles updating the failed login count, inactive user notifications, and logging failed authentications. """ if user: if not user.is_active: record, _ = LoginFailures.objects.get_or_create(user=user) record.failure_count = record.failure_count + 1 log.info('count--> %s',record.failure_count) if record.failure_count <= 5: if record.failure_count < 5: log.info('count1--> %s',record.failure_count) if LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user) return user elif record.failure_count == 5: log.info('count2--> %s',record.failure_count) record.failure_count = record.failure_count #record.lockout_until = datetime.now(UTC) record.save() return user else: _log_and_raise_inactive_user_auth_error(user)
def _handle_failed_authentication(user, authenticated_user): """ Handles updating the failed login count, inactive user notifications, and logging failed authentications. """ failure_count = 0 if user: if LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user) if authenticated_user and not user.is_active: _log_and_raise_inactive_user_auth_error(user) # if we didn't find this username earlier, the account for this email # doesn't exist, and doesn't have a corresponding password loggable_id = user.id if user else "<unknown>" AUDIT_LOG.warning( f"Login failed - password for user.id: {loggable_id} is invalid") 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 error_message = 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) raise AuthFailedError(error_message, error_code='failed-login-attempt', context={ 'remaining_attempts': remaining_attempts, 'allowed_failure_attempts': max_failures_allowed, 'failure_count': failure_count, }) _generate_locked_out_error_message() raise AuthFailedError( _('Email or password is incorrect.'), error_code='incorrect-email-or-password', context={'failure_count': failure_count}, )
def _handle_failed_authentication(user, authenticated_user): """ Handles updating the failed login count, inactive user notifications, and logging failed authentications. """ if user: if LoginFailures.is_feature_enabled(): LoginFailures.increment_lockout_counter(user) if authenticated_user and not user.is_active: _log_and_raise_inactive_user_auth_error(user) # if we didn't find this username earlier, the account for this email # doesn't exist, and doesn't have a corresponding password if settings.FEATURES['SQUELCH_PII_IN_LOGS']: loggable_id = user.id if user else "<unknown>" AUDIT_LOG.warning(u"Login failed - password for user.id: {0} is invalid".format(loggable_id)) 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 if not should_redirect_to_logistration_mircrofrontend: # pylint: disable=no-else-raise 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: raise AuthFailedError(Text(_('Email or password is incorrect.\n' 'You have {remaining_attempts} more sign-in ' 'attempts before your account is temporarily locked.\n' 'If you{quote}ve forgotten your password, click ' '{link_start}here{link_end} to reset.\n' )) .format( quote=HTML("'"), link_start=HTML('<a href="/reset" >'), link_end=HTML('</a>'), remaining_attempts=remaining_attempts)) else: _generate_locked_out_error_message() raise AuthFailedError(_('Email or password is incorrect.'))
def post(self, request, **kwargs): """ Reset learner password using passed token and new credentials """ reset_status = False uidb36 = kwargs.get('uidb36') token = kwargs.get('token') has_required_values, uid_int = self._check_token_has_required_values( uidb36, token) if not has_required_values: AUDIT_LOG.exception("Invalid password reset confirm token") return Response({'reset_status': reset_status}) request.data._mutable = True # lint-amnesty, pylint: disable=protected-access request.data['new_password1'] = normalize_password( request.data['new_password1']) request.data['new_password2'] = normalize_password( request.data['new_password2']) password = request.data['new_password1'] try: user = User.objects.get(id=uid_int) if not default_token_generator.check_token(user, token): AUDIT_LOG.exception("Token validation failed") return Response({'reset_status': reset_status}) validate_password(password, user=user) if settings.ENABLE_AUTHN_RESET_PASSWORD_HIBP_POLICY: # Checks the Pwned Databases for password vulnerability. pwned_response = check_pwned_password(password) if pwned_response.get('vulnerability', 'no') == 'yes': error_status = { 'reset_status': reset_status, 'err_msg': accounts.AUTHN_PASSWORD_COMPROMISED_MSG } return Response(error_status) form = SetPasswordForm(user, request.data) if form.is_valid(): form.save() reset_status = True if 'is_account_recovery' in request.GET: try: old_primary_email = user.email user.email = user.account_recovery.secondary_email user.account_recovery.delete() # emit an event that the user changed their secondary email to the primary email tracker.emit( SETTING_CHANGE_INITIATED, { "setting": "email", "old": old_primary_email, "new": user.email, "user_id": user.id, }) user.save() except ObjectDoesNotExist: err = 'Account recovery process initiated without AccountRecovery instance for user {username}' log.error(err.format(username=user.username)) # Handles clearing the failed login counter upon password reset. if LoginFailures.is_feature_enabled(): LoginFailures.clear_lockout_counter(user) send_password_reset_success_email(user, request) update_session_auth_hash(request, user) except ValidationError as err: AUDIT_LOG.exception("Password validation failed") error_status = { 'reset_status': reset_status, 'err_msg': ' '.join(err.messages) } return Response(error_status) except Exception: # pylint: disable=broad-except AUDIT_LOG.exception("Setting new password failed") return Response({'reset_status': reset_status})