{"id":26648342,"url":"https://github.com/kencochrane/django-defender","last_synced_at":"2025-03-25T00:01:58.775Z","repository":{"id":21002902,"uuid":"24293470","full_name":"jazzband/django-defender","owner":"jazzband","description":"A simple super fast django reusable app that blocks people from brute forcing login attempts","archived":false,"fork":false,"pushed_at":"2024-02-26T16:42:33.000Z","size":426,"stargazers_count":1061,"open_issues_count":29,"forks_count":143,"subscribers_count":19,"default_branch":"master","last_synced_at":"2025-03-17T11:01:54.560Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jazzband.png","metadata":{"files":{"readme":"README.rst","changelog":"CHANGES.rst","contributing":"CONTRIBUTING.rst","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"custom":["https://jazzband.co/donate"]}},"created_at":"2014-09-21T14:42:19.000Z","updated_at":"2025-03-07T13:50:55.000Z","dependencies_parsed_at":"2024-02-15T00:24:21.414Z","dependency_job_id":"3b7ac2f2-607c-4d47-bc6c-f203cb5ae863","html_url":"https://github.com/jazzband/django-defender","commit_stats":{"total_commits":276,"total_committers":57,"mean_commits":4.842105263157895,"dds":0.7101449275362319,"last_synced_commit":"cc6145b84ec73eca5ed9aac521975cad46b23542"},"previous_names":["kencochrane/django-defender"],"tags_count":24,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazzband%2Fdjango-defender","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazzband%2Fdjango-defender/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazzband%2Fdjango-defender/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazzband%2Fdjango-defender/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jazzband","download_url":"https://codeload.github.com/jazzband/django-defender/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245372377,"owners_count":20604491,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-25T00:01:45.371Z","updated_at":"2025-03-25T00:01:58.752Z","avatar_url":"https://github.com/jazzband.png","language":"Python","funding_links":["https://jazzband.co/donate"],"categories":["Python","Libs"],"sub_categories":["Session management"],"readme":"\ndjango-defender\n===============\n\n.. image:: https://jazzband.co/static/img/badge.svg\n   :target: https://jazzband.co/\n   :alt: Jazzband\n\n.. image:: https://img.shields.io/pypi/pyversions/django-defender.svg\n    :alt: Supported Python versions\n    :target: https://pypi.org/project/django-defender/\n\n.. image:: https://img.shields.io/pypi/djversions/django-defender.svg\n   :target: https://pypi.org/project/django-defender/\n   :alt: Supported Django versions\n\n.. image:: https://github.com/jazzband/django-defender/workflows/Test/badge.svg\n   :target: https://github.com/jazzband/django-defender/actions\n   :alt: GitHub Actions\n\n.. image:: https://codecov.io/gh/jazzband/django-defender/branch/master/graph/badge.svg\n   :target: https://codecov.io/gh/jazzband/django-defender\n   :alt: Coverage\n\n.. image:: https://readthedocs.org/projects/django-defender/badge/?version=latest\n   :alt: Documentation Status\n   :target: https://django-defender.readthedocs.io/en/latest/?badge=latest\n\n\nA simple Django reusable app that blocks people from brute forcing login\nattempts. The goal is to make this as fast as possible, so that we do not\nslow down the login attempts.\n\nWe will use a cache so that it doesn't have to hit the database in order to\ncheck the database on each login attempt. The first version will be based on\nRedis, but the goal is to make this configurable so that people can use whatever\nbackend best fits their needs.\n\n\nSites using django-defender\n---------------------------\n\nIf you are using defender on your site, submit a PR to add to the list.\n\n* https://hub.docker.com\n* https://www.mycosbuilder.com\n\n\nDocumentation\n-------------\n\nDocumentation is available on Read the Docs:\n\nhttps://django-defender.readthedocs.io\n\n\nFeatures\n--------\n\n* Log all login attempts to the database\n* Support for reverse proxies with different headers for IP addresses\n* Rate limit based on\n\n    * Username\n    * IP address\n\n* Use Redis for the blacklist\n* Configuration\n\n    * Redis server\n\n        * Host\n        * Port\n        * Database\n        * Password\n        * Key prefix\n\n    * Block length\n\n    * Number of incorrect attempts before block\n\n* 95% code coverage\n* Full documentation\n* Ability to store login attempts to the database\n* Management command to clean up login attempts database table\n* Admin pages\n\n    * List of blocked usernames and IP addresses\n    * List of recent login attempts\n    * Ability to unblock people\n\n* Can be easily adapted to custom authentication method.\n* Signals are sent when blocking username or IP\n\n\nAdmin pages\n***********\n\n.. image:: https://cloud.githubusercontent.com/assets/261601/5950540/8895b570-a729-11e4-9dc3-6b00e46c8043.png\n   :target: https://cloud.githubusercontent.com/assets/261601/5950540/8895b570-a729-11e4-9dc3-6b00e46c8043.png\n   :alt: alt tag\n\n.. image:: https://cloud.githubusercontent.com/assets/261601/5950541/88a35194-a729-11e4-981b-3a55b44ef9d5.png\n   :target: https://cloud.githubusercontent.com/assets/261601/5950541/88a35194-a729-11e4-981b-3a55b44ef9d5.png\n   :alt: alt tag\n\n\nRequirements\n------------\n\n* Python: 3.7, 3.8, 3.9, 3.10, PyPy\n* Django: 3.x, 4.x\n* Redis: 5.x, 6.x, 7.x\n\n\nInstallation\n------------\n\nDownload code, and run setup in one of the following ways depending on the method.\n\nTo install the production ready version from PyPI:\n\n.. code-block:: bash\n\n   pip install django-defender\n\nTo install the development version from source code after download:\n\n.. code-block:: bash\n\n   python setup.py install\n\nTo install the master branch development version from the GitHub repository:\n\n.. code-block:: bash\n\n   pip install -e git+http://github.com/kencochran django-defender.git#egg=django_defender-dev\n\nFirst of all, you must add this project to your list of ``INSTALLED_APPS`` in\n``settings.py``\n\n.. code-block:: python\n\n   INSTALLED_APPS = [\n       'django.contrib.admin',\n       'django.contrib.auth',\n       'django.contrib.contenttypes',\n       'django.contrib.sessions',\n       'django.contrib.sites',\n       # ...\n       'defender',\n       # ...\n   ]\n\nNext, install the ``FailedLoginMiddleware`` middleware\n\n.. code-block:: python\n\n   MIDDLEWARE_CLASSES = [\n       'django.middleware.common.CommonMiddleware',\n       'django.contrib.sessions.middleware.SessionMiddleware',\n       'django.contrib.auth.middleware.AuthenticationMiddleware',\n       'defender.middleware.FailedLoginMiddleware',\n   ]\n\nIf you want to manage the blocked users via the Django admin, then add the\nfollowing to your ``urls.py``\n\n.. code-block:: python\n\n   urlpatterns = [\n       path('admin/defender/', include('defender.urls')), # defender admin\n       path('admin/', admin.site.urls), # normal admin\n       # your own patterns follow...\n   ]\n\n\nMigrations\n**********\n\nYou will need to create tables in your database that are necessary\nfor operation.\n\n.. code-block:: bash\n\n   python manage.py migrate defender\n\n\nManagement commands\n*******************\n\n``cleanup_django_defender``\n\nIf you have a website with a lot of traffic, the AccessAttempts table will get\nfull pretty quickly. If you don't need to keep the data for auditing purposes\nthere is a management command to help you keep it clean.\n\nIt will look at your ``DEFENDER_ACCESS_ATTEMPT_EXPIRATION`` setting to determine\nwhich records will be deleted. Default if not specified, is 24 hours.\n\n.. code-block:: bash\n\n   $ python manage.py cleanup_django_defender\n\nYou can set this up as a daily or weekly cron job to keep the table size down.\n\n.. code-block:: bash\n\n   # run at 12:24 AM every morning.\n   24 0 * * * /usr/bin/python manage.py cleanup_django_defender \u003e\u003e /var/log/django_defender_cleanup.log\n\n\nLong term goals\n---------------\n\n* Pluggable backends, so people can use something other than Redis\n* Email users when their account is blocked\n* Add a whitelist for username and ip's that we will never block (admin's, etc)\n* Add a permanent black list for IP addresses\n* Scan for known proxy IPs and do not block requests coming from those\n  (improve the chances that a good IP is blocked)\n* Add management command to prune old (configurable) login attempts.\n\n\nPerformance\n***********\n\nThe goal of defender is to make it as fast as possible so that it doesn't slow\ndown the login process. In order to make sure our goals are met we need a way\nto test the application to make sure we are on the right track. The best\nway to do this is to compare how fast a normal Django login takes with defender\nand django-axes.\n\nThe normal django login, would be our baseline, and we expect it to be the\nfastest of the 3 methods, because there are no additional checks happening.\n\nThe defender login would most likely be slower then the django login, and\nhopefully faster then the django-axes login. The goal is to make it as little\nof a difference between the regular raw login, and defender.\n\nThe django-axes login speed, will probably be the slowest of the three since it\ndoes more checks and does a lot of database queries.\n\nThe best way to determine the speed of a login is to do a load test against an\napplication with each setup, and compare the login times for each type.\n\n\nLoad testing\n************\n\nIn order to make sure we cover all the different types of logins, in our load\ntest we need to have more then one test.\n\n#. All success: We will do a load test with nothing but successful logins.\n\n#. Mixed: some success some failure: We will load test with some successful logins and some failures to see how the failure effect the performance.\n\n#. All Failures: We will load test with all failure logins and see the difference in performance.\n\nWe will need a sample application that we can use for the load test, with the\nonly difference is the configuration where we either load defender, axes, or\nnone of them.\n\nWe can use a hosted load testing service, or something like jmeter. Either way\nwe need to be consistent for all of the tests. If we use jmeter, we should have\nour jmeter configuration for others to run the tests on their own.\n\n\nResults of load tests\n*********************\n\nWe will post the results here. We will explain each test, and show the results\nalong with some charts.\n\n\nWhy not django-axes\n-------------------\n\ndjango-axes is great but it puts everything in the database, and this causes\na bottle neck when you have a lot of data. It slows down the auth requests by\nas much as 200-300ms. This might not be much for some sites, but for others it\nis too long.\n\nThis started out as a fork of django-axes, and is using as much of their code\nas possible, and removing the parts not needed, and speeding up the lookups\nto improve the login.\n\n\nHow django-defender works\n-------------------------\n\n#. When someone tries to login, we first check to see if they are currently\n   blocked. We check the username they are trying to use, as well as the IP\n   address. If they are blocked, goto step 5. If not blocked go to step 2.\n\n#. They are not blocked, so we check to see if the login was valid. If valid\n   go to step 6. If not valid go to step 3.\n\n#. Login attempt wasn't valid. Add their username and IP address for this\n   attempt to the cache. If this brings them over the limit, add them to the\n   blocked list, and then goto step 5. If not over the limit goto step 4.\n\n#. Login was invalid, but not over the limit. Send them back to the login screen\n   to try again.\n\n#. User is blocked: Send them to the blocked page, telling them they are\n   blocked, and give an estimate on when they will be unblocked.\n\n#. Login is valid. Reset any failed login attempts, and forward to their\n   destination.\n\n\nCache backend\n-------------\n\nDefender uses the cache to save the failed attempts.\n\n\nCache keys\n**********\n\nCounters:\n\n* prefix:failed:ip:[ip] (count, TTL)\n* prefix:failed:username:[username] (count, TTL)\n\nBooleans (if present it is blocked):\n\n* prefix:blocked:ip:[ip] (true, TTL)\n* prefix:blocked:username:[username] (true, TTL)\n\n\nCustomizing django-defender\n---------------------------\n\nYou have a couple options available to you to customize ``django-defender`` a bit.\nThese should be defined in your ``settings.py`` file.\n\n* ``DEFENDER_LOGIN_FAILURE_LIMIT``\\ : Int: The number of login attempts allowed before a\n  record is created for the failed logins.  [Default: ``3``\\ ]\n* ``DEFENDER_LOGIN_FAILURE_LIMIT_USERNAME``\\ : Int: The number of login attempts allowed\n  on a username before a record is created for the failed logins.  [Default: ``DEFENDER_LOGIN_FAILURE_LIMIT``\\ ]\n* ``DEFENDER_LOGIN_FAILURE_LIMIT_IP``\\ : Int: The number of login attempts allowed\n  from an IP before a record is created for the failed logins.  [Default: ``DEFENDER_LOGIN_FAILURE_LIMIT``\\ ]\n* ``DEFENDER_BEHIND_REVERSE_PROXY``\\ : Boolean: Is defender behind a reverse proxy?\n  [Default: ``False``\\ ]\n* ``DEFENDER_REVERSE_PROXY_HEADER``\\ : String: the name of the http header with your\n  reverse proxy IP address  [Default: ``HTTP_X_FORWARDED_FOR``\\ ]\n* ``DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME``\\ : Boolean: Locks a user out based on a combination of IP and Username.  This stops a user denying access to the application for all other users accessing the app from behind the same IP address. [Default: ``False``\\ ]\n* ``DEFENDER_DISABLE_IP_LOCKOUT``\\ : Boolean: If this is True, it will not lockout the users IP address, it will only lockout the username. [Default: False]\n* ``DEFENDER_DISABLE_USERNAME_LOCKOUT``\\ : Boolean: If this is True, it will not lockout usernames, it will only lockout IP addresess. [Default: False]\n* ``DEFENDER_COOLOFF_TIME``\\ : Int: If set, defines a period of inactivity after which\n  old failed login attempts and username/ip lockouts will be forgotten. An integer,\n  will be interpreted as a number of seconds. If 0, neither the failed login attempts\n  nor the username/ip locks will expire. [Default: ``300``\\ ]\n* ``DEFENDER_ATTEMPT_COOLOFF_TIME``\\ : Int: If set, overrides the period of inactivity\n  after which old failed login attempts will be forgotten set by DEFENDER_COOLOFF_TIME.\n  An integer, will be interpreted as a number of seconds. If 0, the failed login\n  attempts will not expire. [Default: ``DEFENDER_COOLOFF_TIME``\\ ]\n* ``DEFENDER_LOCKOUT_COOLOFF_TIME``\\ : Int or List: If set, overrides the period of\n  inactivity after which username/ip lockouts will be forgotten set by\n  DEFENDER_COOLOFF_TIME. An integer, will be interpreted as a number of seconds.\n  A list of integers, will be interpreted as a number of seconds for users with\n  the integer's index being how many previous lockouts (up to some maximum) occurred\n  in the last ``DEFENDER_ACCESS_ATTEMPT_EXPIRATION`` hours. If the property is set to\n  0 or [], the username/ip lockout will not expire. [Default: ``DEFENDER_COOLOFF_TIME``\\ ]\n* ``DEFENDER_LOCKOUT_TEMPLATE``\\ : String:   [Default: ``None``\\ ] If set, specifies a template to render when a user is locked out. Template receives the following context variables:\n\n  * ``cooloff_time_seconds``\\ : The cool off time in seconds\n  * ``cooloff_time_minutes``\\ : The cool off time in minutes\n  * ``failure_limit``\\ : The number of failures before you get blocked.\n\n* ``DEFENDER_USERNAME_FORM_FIELD``\\ : String: the name of the form field that contains your\n  users usernames. [Default: ``username``\\ ]\n* ``DEFENDER_CACHE_PREFIX``\\ : String: The cache prefix for your defender keys.\n  [Default: ``defender``\\ ]\n* ``DEFENDER_LOCKOUT_URL``\\ : String: The URL you want to redirect to if someone is\n  locked out.\n* ``DEFENDER_REDIS_URL``\\ : String: the redis url for defender.\n  [Default: ``redis://localhost:6379/0``\\ ]\n  (Example with password: ``redis://:mypassword@localhost:6379/0``\\ )\n* ``DEFENDER_REDIS_PASSWORD_QUOTE``\\ : Boolean: if special character in redis password (like '@'), we can quote password ``urllib.parse.quote(\"password!@#\")``, and set to True.\n  [Default: ``False``\\ ]\n* ``DEFENDER_REDIS_NAME``\\ : String: the name of the cache from ``CACHES`` in your Django settings (e.g. ``\"default\"``). If set, ``DEFENDER_REDIS_URL`` will be ignored.\n  [Default: ``None``\\ ]\n* ``DEFENDER_STORE_ACCESS_ATTEMPTS``\\ : Boolean: If you want to store the login\n  attempt to the database, set to True. If False, it is not saved\n  [Default: ``True``\\ ]\n* ``DEFENDER_USE_CELERY``\\ : Boolean: If you want to use Celery to store the login\n  attempt to the database, set to True. If False, it is saved inline.\n  [Default: ``False``\\ ]\n* ``DEFENDER_ACCESS_ATTEMPT_EXPIRATION``\\ : Int: Length of time in hours for how\n  long to keep the access attempt records in the database before the management\n  command cleans them up.\n  [Default: ``24``\\ ]\n* ``DEFENDER_GET_USERNAME_FROM_REQUEST_PATH``\\ : String: The import path of the function that access username from request.\n  If you want to use custom function to access and process username from request - you can specify it here.\n  [Default: ``defender.utils.username_from_request``\\ ]\n\n\nRationale for using DEFENDER_ATTEMPT_COOLOFF_TIME and DEFENDER_LOCKOUT_COOLOFF_TIME\n***********************************************************************************\n\nWhile using ``DEFENDER_COOLOFF_TIME`` alone is sufficent for most use cases, when using ``defender`` in some specific scenarios such as in a high security setting, developers may wish to have finer\ngrained control over how long invalid login attempts are \"remembered\" while under consideration for lockout compared to the time those lockout keys are actually locked out from the system.\n``DEFENDER_ATTEMPT_COOLOFF_TIME`` and ``DEFENDER_LOCKOUT_COOLOFF_TIME`` allow for this exact fine grained configuration.\n\nWe can also take a low security and low scale example like a high school's website. Such a website might be run on some of the school's computers and administrated by the school's IT staff and computer\nscience teachers (if lucky enough to have any). In this scenario we can imagine that there are significant portions of the website accessible without authentication, but logging in to the website could\nprovide access to some relatively privileged information such as the student's name, email, grades, and class schedule. Finally since there is an email linked with the account, we will assume that there\nis password reset functionality which unblocks the account when completed. In such a case, one could imagine that there is no need to remember failed logins for long periods of time since the application\nwould simply wish to protect against potential denial of service attacks. This could be accomplished keeping ``DEFENDER_ATTEMPT_COOLOFF_TIME`` low, say 30 seconds, and setting ``DEFENDER_LOCKOUT_COOLOFF_TIME``\nto something much higher like 600 seconds. By keeping ``DEFENDER_ATTEMPT_COOLOFF_TIME`` low and locking out bad actors for significant periods of time by setting ``DEFENDER_LOCKOUT_COOLOFF_TIME`` high,\nrapid brute force login attacks will still be defeated and their small server will have more space in their cache for other data. And by providing password reset functionality as described above, these hypothetical\nadministrators could limit their required involvement in unblocking real users while retaining the intended accessibility of their website.\n\nWhile the previous example is somewhat contrived, the full power of these configurations is demonstrated with the following explanation and example.\n\nWhen ``DEFENDER_STORE_ACCESS_ATTEMPTS`` is True, ``DEFENDER_LOCKOUT_COOLOFF_TIME`` can also be configured as a list of integers. When configured as a list,\nthe number of previous failed login attempts for the configured lockout key is divided by ``DEFENDER_LOGIN_FAILURE_LIMIT`` to produce an intentionally overestimated count\nof the number of failed logins for the period defined by ``DEFENDER_ACCESS_ATTEMPT_EXPIRATION``. This ends up being an overestimate because the time between the failed login attempts\nis not considered when doing this calculation. While this may seem harsh, in some specific scenarios the additional protection against slower attacks can be worth the\\ potential\\ inconvenience\ncaused to real users of the system.\n\nOne such example of this could be a public web accessible web application that houses sensitive information of it's users (let's say personal financial records).\nThe application and data therein should be accessible with minimal interruption, however security is integral so delays can be tolerated up to a point.\nUnder these circumstances we may have a desire to simply set ``DEFENDER_COOLOFF_TIME`` to a very large integer or even 0 for maximum protection. But this would mean that\nif a real user\\ does\\ get locked out of the system, we will need an administrator to manually unblock them which of course is cumbersome and costly.\nBy setting ``DEFENDER_ATTEMPT_COOLOFF_TIME`` to a large enough number, let's say 600 and setting ``DEFENDER_LOCKOUT_COOLOFF_TIME`` to a list of increasing integers (ie. [60, 120, 300, 600, 0]) we can\nprotect our theoretical application comprably to if we had simply set ``DEFENDER_COOLOFF_TIME`` to 600 while disrupting our users significantly less.\n\n\nAdapting to other authentication methods\n----------------------------------------\n\n``defender`` can be used for authentication other than ``Django authentication system``.\nE.g. if ``django-rest-framework`` authentication has to be protected from brute force attack, a custom authentication method can be implemented.\n\nThere's sample ``BasicAuthenticationDefender`` class based on ``djangorestframework.BasicAuthentication``\\ :\n\n.. code-block:: python\n\n   import base64\n   import binascii\n\n   from django.utils.translation import gettext_lazy as _\n\n   from rest_framework import HTTP_HEADER_ENCODING, exceptions\n   from rest_framework.authentication import (\n       BasicAuthentication,\n       get_authorization_header,\n   )\n\n   from defender import utils\n   from defender import config\n\n   class BasicAuthenticationDefender(BasicAuthentication):\n\n       def get_username_from_request(self, request):\n           auth = get_authorization_header(request).split()\n           return base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')[0]\n\n       def authenticate(self, request):\n           auth = get_authorization_header(request).split()\n\n           if not auth or auth[0].lower() != b'basic':\n               return None\n\n           if len(auth) == 1:\n               msg = _('Invalid basic header. No credentials provided.')\n               raise exceptions.AuthenticationFailed(msg)\n           elif len(auth) \u003e 2:\n               msg = _('Invalid basic header. Credentials string should not contain spaces.')\n               raise exceptions.AuthenticationFailed(msg)\n\n           if utils.is_already_locked(request, get_username=self.get_username_from_request):\n               detail = \"You have attempted to login {failure_limit} times, with no success.\" \\\n                        \"Your account is locked for {cooloff_time_seconds} seconds\" \\\n                        \"\".format(\n                           failure_limit=config.FAILURE_LIMIT,\n                           cooloff_time_seconds=config.LOCKOUT_COOLOFF_TIME[\n                              defender_utils.get_lockout_cooloff_time(username=self.get_username_from_request(request))\n                           ]\n                        )\n               raise exceptions.AuthenticationFailed(_(detail))\n\n           try:\n               auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')\n           except (TypeError, UnicodeDecodeError, binascii.Error):\n               msg = _('Invalid basic header. Credentials not correctly base64 encoded.')\n               raise exceptions.AuthenticationFailed(msg)\n\n           userid, password = auth_parts[0], auth_parts[2]\n           login_unsuccessful = False\n           login_exception = None\n           try:\n               response = self.authenticate_credentials(userid, password)\n           except exceptions.AuthenticationFailed as e:\n               login_unsuccessful = True\n               login_exception = e\n\n           utils.add_login_attempt_to_db(request,\n                                         login_valid=not login_unsuccessful,\n                                         get_username=self.get_username_from_request)\n           # add the failed attempt to Redis in case of a failed login or resets the attempt count in case of success\n           utils.check_request(request,\n                               login_unsuccessful=login_unsuccessful,\n                               get_username=self.get_username_from_request)\n           if login_unsuccessful:\n               raise login_exception\n\n           return response\n\nTo make it work add ``BasicAuthenticationDefender`` to ``DEFAULT_AUTHENTICATION_CLASSES`` above all other authentication methods in your ``settings.py``.\n\nAdapting to other authentication methods :- django-rest-auth in djangorestframework\n------------------------------------------------------------------------------------\n``defender`` can be incorporated with the combination of ``django-rest-framework`` and ``django-rest-auth`` which can be used to authenticate users.\n\nReference\n**********\n* https://www.django-rest-framework.org/\n* https://django-rest-auth.readthedocs.io/en/latest/\n\nBelow is a sample ``BasicAuthenticationDefender`` class based on ``rest_framework.authentication.TokenAuthentication`` which uses ``django-rest-auth`` library for user authentication.\n\n.. code-block:: python\n\n   import base64\n   import binascii\n\n   from django.conf import settings\n   from django.contrib.auth import get_user_model, authenticate\n   from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm\n   from django.contrib.auth.tokens import default_token_generator\n   from django.utils.http import urlsafe_base64_decode as uid_decoder\n   from django.utils.translation import ugettext_lazy as _\n   from django.utils.encoding import force_text\n   from rest_framework import serializers, exceptions, HTTP_HEADER_ENCODING\n   from rest_framework.exceptions import ValidationError\n   from defender import utils as defender_utils\n   from defender import config\n   from rest_framework.authentication import (\n       get_authorization_header,\n   )\n\n   # Get the UserModel\n   UserModel = get_user_model()\n\n   class BasicAuthenticationDefender(serializers.Serializer):\n\n      username = serializers.CharField(required=False, allow_blank=True)\n      email = serializers.EmailField(required=False, allow_blank=True)\n      password = serializers.CharField(style={'input_type': 'password'})\n\n      def authenticate(self, **kwargs):\n        request = self.context['request']\n\n        if hasattr(settings, 'ACCOUNT_AUTHENTICATION_METHOD'):\n            login_field = settings.ACCOUNT_AUTHENTICATION_METHOD\n        else:\n            login_field = 'username'\n        userid = self.username_from_request(request, login_field)\n\n        if defender_utils.is_already_locked(request, username=userid):\n            detail = \"You have attempted to login {failure_limit} times with no success. \"\n                     .format(\n                         failure_limit=config.FAILURE_LIMIT,\n                         cooloff_time_seconds=config.LOCKOUT_COOLOFF_TIME[defender_utils.get_lockout_cooloff_time(username=userid)]\n                     )\n            raise exceptions.AuthenticationFailed(_(detail))\n\n        login_unsuccessful = False\n        login_exception = None\n        try:\n            response = authenticate(request, **kwargs)\n            if response == None:\n                login_unsuccessful = True\n                msg = _('Unable to log in with provided credentials.')\n                # raise exceptions.ValidationError(msg)\n                login_exception = exceptions.ValidationError(msg)\n        except exceptions.AuthenticationFailed as e:\n            login_unsuccessful = True\n            login_exception = e\n\n        defender_utils.add_login_attempt_to_db(request,\n                                               login_valid=not login_unsuccessful,\n                                               username=userid)\n\n        user_not_blocked = defender_utils.check_request(request,\n                                                        login_unsuccessful=login_unsuccessful,\n                                                        username=userid)\n        if user_not_blocked and not login_unsuccessful:\n            return response\n\n        raise login_exception\n\n      def _validate_email(self, email, password):\n        user = None\n\n        if email and password:\n            user = self.authenticate(email=email, password=password)\n        else:\n            msg = _('Must include \"email\" and \"password\".')\n            raise exceptions.ValidationError(msg)\n\n        return user\n\n      def _validate_username(self, username, password):\n        user = None\n\n        if username and password:\n            user = self.authenticate(username=username, password=password)\n        else:\n            msg = _('Must include \"username\" and \"password\".')\n            raise exceptions.ValidationError(msg)\n\n        return user\n\n      def _validate_username_email(self, username, email, password):\n        user = None\n\n        if email and password:\n            user = self.authenticate(email=email, password=password)\n        elif username and password:\n            user = self.authenticate(username=username, password=password)\n        else:\n            msg = _('Must include either \"username\" or \"email\" and \"password\".')\n            raise exceptions.ValidationError(msg)\n\n        return user\n\n      def validate(self, attrs):\n        username = attrs.get('username')\n        email = attrs.get('email')\n        password = attrs.get('password')\n\n        user = None\n\n        if 'allauth' in settings.INSTALLED_APPS:\n            from allauth.account import app_settings\n\n            # Authentication through email\n            if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.EMAIL:\n                user = self._validate_email(email, password)\n\n            # Authentication through username\n            elif app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME:\n                user = self._validate_username(username, password)\n\n            # Authentication through either username or email\n            else:\n                user = self._validate_username_email(username, email, password)\n\n        else:\n            # Authentication without using allauth\n            if email:\n                try:\n                    username = UserModel.objects.get(\n                        email__iexact=email).username()\n                except UserModel.DoesNotExist:\n                    pass\n\n            if username:\n                user = self._validate_username_email(username, '', password)\n\n        # Did we get back an active user?\n        if user:\n            if not user.is_active:\n                msg = _('User account is disabled.')\n                raise exceptions.ValidationError(msg)\n        else:\n            msg = _('Unable to log in with provided credentials.')\n            raise exceptions.ValidationError(msg)\n\n        # If required, is the email verified?\n        if 'rest_auth.registration' in settings.INSTALLED_APPS:\n            from allauth.account import app_settings\n            if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY:\n                email_address = user.emailaddress_set.get(email=user.email)\n                if not email_address.verified:\n                    raise serializers.ValidationError(\n                        _('E-mail is not verified.'))\n\n        attrs['user'] = user\n        return attrs\n\n      def username_from_request(self, request, login_field):\n        user_data = request._data\n        return user_data[login_field]\n\nTo make it work add ``BasicAuthenticationDefender`` to ``REST_AUTH_SERIALIZERS`` dictionary in your ``settings.py`` under the key ``LOGIN_SERIALIZER``.\nFor example, in your settings.py add the below line,\n\n.. code-block:: python\n\n   REST_AUTH_SERIALIZERS = {\n       'LOGIN_SERIALIZER': '\u003cpath to your basic authentication defender python file\u003e.BasicAuthenticationDefender',\n   }\n\nAdapting for password reset forms\n---------------------------------\n\n``defender`` can be adapted for Django’s ``PasswordResetView`` to prevent too many submissions.\n\nWe need to create some new views that subclass Django’s built-in ``LoginView``, ``PasswordResetView`` \u0026 ``PasswordResetConfirmView`` — then use these views in our ``urls.py`` as replacements for Django’s built-ins.\n\nThe views block based on email address submitted on the password reset view. This is different than the default implementation (which uses username), so we have to be careful to clean up after ourselves on sign-in \u0026 completed password reset.\n\n.. code-block:: python\n\n    from defender import utils as def_utils\n    from django.contrib.auth import views as auth_views\n\n    class UserSignIn(auth_views.LoginView):\n        def form_valid(self, form):\n            \"\"\"Force clear all the cached Defender statues for the authenticated user’s email address.\"\"\"\n            super_valid = super().form_valid(form)\n            def_utils.check_request(self.request, False, username=form.get_user().email)\n            return super_valid\n\n    class PasswordResetBruteForceProtectedView(auth_views.PasswordResetView):\n        def get(self, request, *args, **kwargs):\n            \"\"\"Confirm the user isn’t already blocked by IP before showing the password reset view.\"\"\"\n            if def_utils.is_already_locked(request):\n                return def_utils.lockout_response(request)\n            return super().get(request, *args, **kwargs)\n\n        def post(self, request, *args, **kwargs):\n            \"\"\"\n            Confirm the user isn’t already blocked by IP before allowing form POST.\n\n            Also, force log this form POST as a single entry in the Defender cache, against the submitted email address.\n            \"\"\"\n            if def_utils.is_already_locked(request):\n                return def_utils.lockout_response(request)\n            def_utils.check_request(\n                request, login_unsuccessful=True, username=request.POST.get(\"email\")\n            )\n            return super().post(request, *args, **kwargs)\n\n\n    class PasswordResetConfirmBruceForceProtectedView(auth_views.PasswordResetConfirmView):\n        def get(self, request, *args, **kwargs):\n            \"\"\"Confirm the user isn’t already blocked by IP before showing the password confirm view.\"\"\"\n            if def_utils.is_already_locked(request):\n                return def_utils.lockout_response(request)\n            return super().get(request, *args, **kwargs)\n\n        def post(self, request, *args, **kwargs):\n            \"\"\"Confirm the user isn’t already blocked by IP before allowing form POST for the password change confirmation.\"\"\"\n            if def_utils.is_already_locked(request):\n                return def_utils.lockout_response(request)\n            return super().post(request, *args, **kwargs)\n\n        def form_valid(self, form):\n            \"\"\"Force clear all the cached Defender statues for the user’s email address after successfully changing their password.\"\"\"\n            super_valid = super().form_valid(form)\n            def_utils.check_request(\n                self.request, login_unsuccessful=False, username=self.user.email\n            )\n            return super_valid\n\nDjango signals\n--------------\n\n``django-defender`` will send signals when blocking a username or an IP address. To set up signal receiver functions:\n\n.. code-block:: python\n\n   from django.dispatch import receiver\n\n   from defender import signals\n\n   @receiver(signals.username_block)\n   def username_blocked(username, **kwargs):\n       print(\"%s was blocked!\" % username)\n\n   @receiver(signals.ip_block)\n   def ip_blocked(ip_address, **kwargs):\n       print(\"%s was blocked!\" % ip_address)\n\n\nRunning tests\n-------------\n\nTests can be run, after you clone the repository and having Django installed,\nlike:\n\n.. code-block:: bash\n\n   PYTHONPATH=$PYTHONPATH:$PWD django-admin test defender --settings=defender.test_settings\n\nWith Code coverage:\n\n.. code-block:: bash\n\n   PYTHONPATH=$PYTHONPATH:$PWD coverage run --source=defender $(which django-admin) test defender --settings=defender.test_settings\n\n\nReleasing\n---------\n\n#. ``python setup.py sdist``\n#. ``twine upload dist/*``\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkencochrane%2Fdjango-defender","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkencochrane%2Fdjango-defender","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkencochrane%2Fdjango-defender/lists"}