From e7b859d623de9b8d6fc8e614a2264f0a20112614 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Thu, 25 Mar 2021 11:37:54 +0100 Subject: [PATCH] authenticators: attach login failure record to user (#51626) --- src/authentic2/authenticators.py | 6 +++++- src/authentic2/backends/ldap_backend.py | 6 +++++- src/authentic2/backends/models_backend.py | 2 ++ src/authentic2/journal_event_types.py | 4 ++-- tests/test_ldap.py | 20 ++++++++++++++++++++ tests/test_login.py | 5 ++++- 6 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/authentic2/authenticators.py b/src/authentic2/authenticators.py index 0fc67759..49bb8cf4 100644 --- a/src/authentic2/authenticators.py +++ b/src/authentic2/authenticators.py @@ -100,6 +100,7 @@ class LoginPasswordAuthenticator(BaseAuthenticator): data = request.POST if is_post else None initial = {} preferred_ous = [] + request.failed_logins = {} # Special handling when the form contains an OU selector if app_settings.A2_LOGIN_FORM_OU_SELECTOR: @@ -139,7 +140,10 @@ class LoginPasswordAuthenticator(BaseAuthenticator): return response else: username = form.cleaned_data.get('username', '').strip() - if username: + if request.failed_logins: + for user in request.failed_logins.values(): + request.journal.record('user.login.failure', user=user, username=username) + elif username: request.journal.record('user.login.failure', username=username) context['form'] = form return render(request, 'authentic2/login_password_form.html', context) diff --git a/src/authentic2/backends/ldap_backend.py b/src/authentic2/backends/ldap_backend.py index 35b451a0..b611834d 100644 --- a/src/authentic2/backends/ldap_backend.py +++ b/src/authentic2/backends/ldap_backend.py @@ -720,6 +720,10 @@ class LDAPBackend(object): except ldap.INVALID_CREDENTIALS as e: if block.get('use_controls') and len(e.args) > 0 and 'ctrls' in e.args[0]: self.process_controls(request, authz_id, DecodeControlTuples(e.args[0]['ctrls'])) + attributes = self.get_ldap_attributes(block, conn, authz_id) + user = self.lookup_existing_user(authz_id, block, attributes) + if user and hasattr(request, 'failed_logins'): + request.failed_logins[user.uuid] = user user_login_failure(authz_id) pass else: @@ -1234,7 +1238,7 @@ class LDAPBackend(object): for lookup_type in block['lookups']: if lookup_type == 'username': return self.lookup_by_username(username) - elif lookup_type == 'external_id': + elif lookup_type == 'external_id' and attributes: return self.lookup_by_external_id(block, attributes) def update_user_identifiers(self, user, username, block, attributes): diff --git a/src/authentic2/backends/models_backend.py b/src/authentic2/backends/models_backend.py index 61604c5c..7bf93305 100644 --- a/src/authentic2/backends/models_backend.py +++ b/src/authentic2/backends/models_backend.py @@ -83,6 +83,8 @@ class ModelBackend(ModelBackend): return user else: user_login_failure(user.get_username()) + if hasattr(request, 'failed_logins'): + request.failed_logins[user.uuid] = user def get_user(self, user_id): UserModel = get_user_model() diff --git a/src/authentic2/journal_event_types.py b/src/authentic2/journal_event_types.py index 6fa89fb7..b1164582 100644 --- a/src/authentic2/journal_event_types.py +++ b/src/authentic2/journal_event_types.py @@ -149,8 +149,8 @@ class UserLoginFailure(EventTypeWithService): label = _('login failure') @classmethod - def record(cls, service, username): - super().record(service=service, data={'username': username}) + def record(cls, service, username, user): + super().record(user=user, service=service, data={'username': username}) @classmethod def get_message(cls, event, context): diff --git a/tests/test_ldap.py b/tests/test_ldap.py index aa0fba23..3d88f05d 100644 --- a/tests/test_ldap.py +++ b/tests/test_ldap.py @@ -258,6 +258,26 @@ def test_double_login(slapd, simple_user, settings, app, db): utils.login(app, UID, password=PASS, path='/admin/') +def test_login_failure(slapd, simple_user, settings, app, db): + settings.LDAP_AUTH_SETTINGS = [{ + 'url': [slapd.ldap_url], + 'basedn': u'o=ôrga', + 'use_tls': False, + 'is_superuser': True, + 'is_staff': True, + }] + # create ldap user + utils.login(app, UID, password=PASS, path='/admin/') + utils.logout(app) + user = ldap_backend.LDAPUser.objects.get(username='%s@ldap' % UID) + + utils.login(app, simple_user, password='wrong', fail=True) + utils.assert_event('user.login.failure', user=simple_user, username=simple_user.username) + + utils.login(app, UID, password='wrong', fail=True) + utils.assert_event('user.login.failure', user=user, username=UID) + + def test_keep_password_in_session(slapd, settings, client, db): settings.LDAP_AUTH_SETTINGS = [{ 'url': [slapd.ldap_url], diff --git a/tests/test_login.py b/tests/test_login.py index 21ec7629..535e28bb 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -36,7 +36,10 @@ def test_success(db, app, simple_user): def test_failure(db, app, simple_user): login(app, simple_user, password='wrong', fail=True) - assert_event('user.login.failure', username=simple_user.username) + assert_event('user.login.failure', user=simple_user, username=simple_user.username) + + login(app, 'noone', password='wrong', fail=True) + assert_event('user.login.failure', username='noone') def test_login_inactive_user(db, app): -- 2.20.1