Skip to main content

Django app to manage account login throttling.

Project description

django-account-locker

Django app for managing failed logins and account lockout.

Compatibility

This package supports:

  • Python 3.12+ and Django 5.2-6.0

Background

Email / password logins (without MFA) are vulnerable to Brute Force attacks where a malicious party can attempt to crack the password by cycling through a list of password for a given username (email).

One mitigation for this is "Account lockout", whereby an account is locked when a certain threshold of X failures in Y time period is exceeded. This is what this package implements, in its simplest possible form.

Note on Account Lockouts

OWASP itself is equivocal on the subject of account lockouts, as they can be used in extremis to DOS a service by locking out all of their users, and overwhelming their support team with requests to unlock.

https://owasp.org/www-community/controls/Blocking_Brute_Force_Attacks

Use with caution, and if in doubt use something additional measures like MFA, or remove passwords altogether and use SSO, Passkeys etc.

Implementation

This packages satisfies two requirements:

  1. Log all failures for future reference / investigation
  2. Apply temporary lock to prevent further logins for a period of time.

Failure logging

This package includes a model called FailedLogin which records the username and request info (IP address, user agent).

NB This locking mechanism operates at the username level, and not a the User account level. This is to prevent another attack, Account Enumeration, whereby an attacker can determine which accounts are real.

This package locks the string used as the username - it makes no difference whether that relates to a real account or not. It is essentially saying "You cannot continue to try this username for a period".

Account lockout

The lockout process is very simple and backed by the Django cache. When a failed login tips the account over the threshold a cache entry is set for the period configured as the lockout, and if that cache entry exists all further login attempts can be ignored.

Configuration

There are three settings that manage the threshold. The default threshold is "4 failed logins in 60 seconds locks the account for 60 seconds". The individual settings are below.

MAX_FAILED_LOGIN_ATTEMPTS

The number of failed logins within the FAILED_LOGIN_INTERVAL_SECS required to trip a lockout. Defaults to 4.

FAILED_LOGIN_INTERVAL_SECS

The interval over which failed logins should be considered - e.g. if the threshold is "3 attempts in 30s", this value is 30. Defaults to 60.

ACCOUNT_LOCKED_TIMEOUT_SECS

The duration (in seconds) of the lockout period, in the event that the number of failed logins within the FAILED_LOGIN_INTERVAL_SECS exceeds the limit set by MAX_FAILED_LOGIN_ATTEMPTS. Defaults to 60.

Demo app

The actual login class is left out of the core package, and is up to you to implement. The demo app provided in the source distribution does include an authentication backend called CustomAuthBackend which demonstrates a very simple implementation.

class CustomAuthBackend(ModelBackend):
    def authenticate(
        self,
        request: HttpRequest,
        username: str | None = None,
        password: str | None = None,
        **kwargs: Any,
    ) -> settings.AUTH_USER_MODEL | None:

        # if the username is already locked - ignore authentication
        if lockout.is_account_locked(username):
            logger.info("Account is locked")
            messages.error(request, "Your account is locked.")
            return None

        # attempt to authenticate normally
        try:
            user = User.objects.get(username=username)
            if user.check_password(password):
                return user
            # password supplied was invalid - log and continue
            logger.info("Invalid password for user %s", username)
            messages.error(request, "Invalid username / password combination.")
        except User.DoesNotExist:
            # username supplied was invalid - log and continue
            logger.info("Invalid username %s", username)
            messages.error(request, "Invalid username / password combination.")

        # either username or login failed - record the login
        # this will return True if the account has been locked
        lockout.handle_failed_login(username, request)
        if lockout.is_account_locked(username):
            messages.error(request, "Your account has been locked.")
        return None

If you manage your login failure using exceptions, you can use the raise_if_locked method:

class CustomAuthBackend(ModelBackend):
    def authenticate(
        self,
        request: HttpRequest,
        username: str | None = None,
        password: str | None = None,
        **kwargs: Any,
    ) -> settings.AUTH_USER_MODEL | None:

        # if the username is already locked raise AccountLocked
        lockout.raise_if_locked(username)

        try:
            user = User.objects.get(username=username)
            if user.check_password(password):
                return user
        except User.DoesNotExist:
            lockout.handle_failed_login(username, request):

        # have we now tripped the AccountLocked exception?
        lockout.raise_if_locked(username)

All of this is wrapped up in a decorator called apply_account_lock, which wraps the ModelBackend.authenticate method. The simplest possible implementation is therefore:

class CustomModelBackend(ModelBackend):
    @apply_account_lock
    def authenticate(self, request, **credentials) -> User | None:
        super().authenticate(self, **credentials):

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_account_locker-0.3.0.tar.gz (6.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_account_locker-0.3.0-py3-none-any.whl (9.5 kB view details)

Uploaded Python 3

File details

Details for the file django_account_locker-0.3.0.tar.gz.

File metadata

  • Download URL: django_account_locker-0.3.0.tar.gz
  • Upload date:
  • Size: 6.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for django_account_locker-0.3.0.tar.gz
Algorithm Hash digest
SHA256 f2b709d1c2555d97851b96c47d93813fe5cc764dbd795880bc4c4bc83d05e9b8
MD5 1895628f6f8cacdefd7540a39aebc3e7
BLAKE2b-256 44f623a3fcfd766c71c26eb47263606ca72800f30b8d5281aa7532d63931221f

See more details on using hashes here.

File details

Details for the file django_account_locker-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_account_locker-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7b9bfbc539d2537ecebf9b2c7f436d5f5405cb19b9336a77cb88dc1d98c4d101
MD5 05847d2d6fee95096260f72ce2356ca0
BLAKE2b-256 105388ad6baa209bd9ec19bff1c7d7a849c2fd7b09b12696f99c00c3c51562f1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page