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:
- Log all failures for future reference / investigation
- 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f2b709d1c2555d97851b96c47d93813fe5cc764dbd795880bc4c4bc83d05e9b8
|
|
| MD5 |
1895628f6f8cacdefd7540a39aebc3e7
|
|
| BLAKE2b-256 |
44f623a3fcfd766c71c26eb47263606ca72800f30b8d5281aa7532d63931221f
|
File details
Details for the file django_account_locker-0.3.0-py3-none-any.whl.
File metadata
- Download URL: django_account_locker-0.3.0-py3-none-any.whl
- Upload date:
- Size: 9.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b9bfbc539d2537ecebf9b2c7f436d5f5405cb19b9336a77cb88dc1d98c4d101
|
|
| MD5 |
05847d2d6fee95096260f72ce2356ca0
|
|
| BLAKE2b-256 |
105388ad6baa209bd9ec19bff1c7d7a849c2fd7b09b12696f99c00c3c51562f1
|