diff --git a/README.md b/README.md
index 4cfad283f905d76a298be7853cb2867d714f41e0..4de44c827ef98a71db2fb83eb50fd885488d7acf 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,8 @@ BASETHEME_BOOTSTRAP_USER_PREFERENCE_MODEL_NAME = "MyUserPreferences"
 BASETHEME_BOOTSTRAP_USERNAME_IS_EMAIL = False
 BASETHEME_BOOTSTRAP_FIRST_LAST_NAME_REQUIRED = False
 BASETHEME_BOOTSTRAP_VALIDATE_EMAIL_BEFORE_ACTIVATION = False
+BASETHEME_BOOTSTRAP_FAILED_LOGIN_EXPONENTIAL_BACKOFF_ENABLED = True
+BASETHEME_BOOTSTRAP_FAILED_LOGIN_EXPONENTIAL_BACKOFF_GRADIENT = 1.47
 
 ################################################################################
 ```
diff --git a/README.rst b/README.rst
index fa7a28b45cd2370b704c75bc4eb3c98039d1c395..8788a90a997852e14a68303830c45f9c76bb9849 100644
--- a/README.rst
+++ b/README.rst
@@ -55,6 +55,8 @@ Quick start
     BASETHEME_BOOTSTRAP_USERNAME_IS_EMAIL = False
     BASETHEME_BOOTSTRAP_FIRST_LAST_NAME_REQUIRED = False
     BASETHEME_BOOTSTRAP_VALIDATE_EMAIL_BEFORE_ACTIVATION = False
+    BASETHEME_BOOTSTRAP_FAILED_LOGIN_EXPONENTIAL_BACKOFF_ENABLED = True
+    BASETHEME_BOOTSTRAP_FAILED_LOGIN_EXPONENTIAL_BACKOFF_GRADIENT = 1.47
 
     ################################################################################
 
diff --git a/basetheme_bootstrap/default_settings.py b/basetheme_bootstrap/default_settings.py
index a6583f05903239995025f6b7d644997ff592ed03..fe6f73026c1dcdbbd79f310b5bd3263ac171d594 100644
--- a/basetheme_bootstrap/default_settings.py
+++ b/basetheme_bootstrap/default_settings.py
@@ -20,3 +20,21 @@ def is_validating_email():
         return settings.BASETHEME_BOOTSTRAP_VALIDATE_EMAIL_BEFORE_ACTIVATION
     except AttributeError:
         return False
+
+
+def is_failed_login_exponential_backoff_enabled() -> bool:
+    try:
+        return settings.BASETHEME_BOOTSTRAP_FAILED_LOGIN_EXPONENTIAL_BACKOFF_ENABLED
+    except AttributeError:
+        return True
+
+
+def get_failed_login_exponential_backoff_gradient() -> float:
+    try:
+        return settings.BASETHEME_BOOTSTRAP_FAILED_LOGIN_EXPONENTIAL_BACKOFF_GRADIENT
+    except AttributeError:
+        # import math
+        # watch_period = 300
+        # max_login_attempt = 15
+        # return math.ceil(math.exp(math.log(watch_period) / max_login_attempt) * 100) / 100
+        return 1.47
diff --git a/basetheme_bootstrap/forms.py b/basetheme_bootstrap/forms.py
index 2c911c8aaf0ddcb5ce46894833b71ddcdcb262ce..50cb2044c4772141a8919d406f8967452715416c 100644
--- a/basetheme_bootstrap/forms.py
+++ b/basetheme_bootstrap/forms.py
@@ -1,12 +1,19 @@
 from django import forms
 from django.contrib.auth import get_user_model, forms as auth_forms
+from django.core.cache import cache
+from django.core.exceptions import ValidationError
 from django.db.models import Q
 from django.forms import widgets
 from django.urls import reverse
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext_lazy as _
 
-from basetheme_bootstrap.default_settings import is_username_is_email, is_first_last_name_required
+from basetheme_bootstrap.default_settings import (
+    is_username_is_email,
+    is_first_last_name_required,
+    get_failed_login_exponential_backoff_gradient,
+    is_failed_login_exponential_backoff_enabled,
+)
 
 
 class CleanUsernameAndSuggestReset:
@@ -94,3 +101,36 @@ class AuthenticationForm(auth_forms.AuthenticationForm):
         super().__init__(*args, **kwargs)
         if is_username_is_email():
             self.fields["username"].label = _("Email")
+
+    def clean(self):
+        if not is_failed_login_exponential_backoff_enabled():
+            return super().clean()
+        ip_address = self.request.META.get("REMOTE_ADDR")
+
+        # Increment the login attempt count for this IP address
+        cache_key = f"login_attempts:{ip_address}"
+        login_attempts = cache.get(cache_key, 0)
+        cache.set(cache_key, login_attempts + 1, 300)
+
+        # check if lock exists for this ip address
+        locked_key = f"locked_login_attempts:{ip_address}"
+        locked_login_attempts = cache.get(locked_key, False)
+        # expiry of the lock is based on the number of already done login_attempts
+        wait_time = min(
+            get_failed_login_exponential_backoff_gradient()
+            ** login_attempts,
+            300,
+        )
+        cache.set(locked_key, True, wait_time)
+
+        # if locked was found we fail the form validation to prevent successful login
+        if locked_login_attempts:
+            m = int(wait_time // 60)
+            s = int(wait_time - 60 * m)
+            # informing the user that zhe has hit the wall and have to wait
+            raise ValidationError(
+                _(
+                    "Too many login attempts ({:d}). Please try again later after {:02d}:{:02d} minute."
+                ).format(login_attempts, m, s)
+            )
+        return super().clean()
diff --git a/basetheme_bootstrap/locale/en/LC_MESSAGES/django.po b/basetheme_bootstrap/locale/en/LC_MESSAGES/django.po
index bb28f0b5f2302da975eea1f1de7b86fee195f0bf..6a65c8bdaee70d33d9ab38826cc2b04571d70fee 100644
--- a/basetheme_bootstrap/locale/en/LC_MESSAGES/django.po
+++ b/basetheme_bootstrap/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-11-17 12:43+0000\n"
+"POT-Creation-Date: 2024-09-03 11:11+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -36,6 +36,11 @@ msgstr ""
 msgid "Email"
 msgstr ""
 
+msgid ""
+"Too many login attempts ({:d}). Please try again later after {:02d}:{:02d} "
+"minute."
+msgstr ""
+
 msgid "Error 403"
 msgstr ""
 
diff --git a/basetheme_bootstrap/locale/fr/LC_MESSAGES/django.mo b/basetheme_bootstrap/locale/fr/LC_MESSAGES/django.mo
index bf8d8ab21436b7a690f6cf4a7c15b9b0bc23a4cd..2f1ba904093ce2f4bfd188bdafe14335fb0ba910 100644
Binary files a/basetheme_bootstrap/locale/fr/LC_MESSAGES/django.mo and b/basetheme_bootstrap/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/basetheme_bootstrap/locale/fr/LC_MESSAGES/django.po b/basetheme_bootstrap/locale/fr/LC_MESSAGES/django.po
index f4e5c1cca1c57abecdc19da9ed1ada8085355e78..d177d18542cd69c16961f6f019c5606c3fbfa3f2 100644
--- a/basetheme_bootstrap/locale/fr/LC_MESSAGES/django.po
+++ b/basetheme_bootstrap/locale/fr/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-11-17 12:43+0000\n"
+"POT-Creation-Date: 2024-09-03 11:54+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -38,6 +38,13 @@ msgstr "Le nom est requis"
 msgid "Email"
 msgstr "Courriel"
 
+msgid ""
+"Too many login attempts ({:d}). Please try again later after {:02d}:{:02d} "
+"minute."
+msgstr ""
+"Il y a trop de tentatives de connection ({:d}). Avant d'essayer de nouveau, "
+"merci d'attendre {:02d}m{:02d}s"
+
 msgid "Error 403"
 msgstr "Erreur 403"
 
diff --git a/basetheme_bootstrap/tests.py b/basetheme_bootstrap/tests.py
index 7bfa7d2959138696ef42509c9380b955ccd8f163..8fbe70db771b08ef270425bc71c7d14b1f00ad8a 100644
--- a/basetheme_bootstrap/tests.py
+++ b/basetheme_bootstrap/tests.py
@@ -1,5 +1,6 @@
 import os
 import re
+from time import sleep
 
 from django.conf import settings
 from django.contrib.auth import get_user_model
@@ -10,7 +11,7 @@ from django.core.exceptions import ValidationError
 from django.test import TestCase, RequestFactory, override_settings
 from django.urls import reverse
 
-from basetheme_bootstrap import user_preferences_utils, tokens
+from basetheme_bootstrap import user_preferences_utils, tokens, default_settings
 from basetheme_bootstrap.user_preferences_utils import get_user_preferences_for_user
 
 
@@ -697,3 +698,101 @@ class TemplatesTagsTestsDebugFalse(TemplatesTagsTests):
 )
 class UserPreferencesTestsDebugFalse(UserPreferencesTests):
     pass
+
+
+class ExponentialBackoffFailedLoginEnabledTests(TestCase):
+    def test_basic(self):
+        self.assertTrue(default_settings.is_failed_login_exponential_backoff_enabled())
+
+
+@override_settings(
+    BASETHEME_BOOTSTRAP_FAILED_LOGIN_EXPONENTIAL_BACKOFF_ENABLED=False,
+)
+class ExponentialBackoffFailedLoginDisabledTests(TestCase):
+    def test_basic(self):
+        self.assertFalse(default_settings.is_failed_login_exponential_backoff_enabled())
+
+
+@override_settings(
+    BASETHEME_BOOTSTRAP_FAILED_LOGIN_EXPONENTIAL_BACKOFF_ENABLED=True,
+)
+class ExponentialBackoffFailedLoginEnabledTests(TestCase):
+    def test_basic(self):
+        self.assertTrue(default_settings.is_failed_login_exponential_backoff_enabled())
+
+
+class ExponentialBackoffFailedLoginTests(TestCase):
+    def setUp(self):
+        cache.clear()
+        self.user_pwd = "eil2guj4cuSho2Vai3hu"
+        self.user = get_user_model().objects.create(
+            username="a",
+            email="a@a.a",
+        )
+        self.user.set_password(self.user_pwd)
+        self.user.save()
+
+    def test_login_ok(self):
+        data = {
+            'username': self.user.username,
+            'email': self.user.email,
+            'password': self.user_pwd,
+        }
+        response = self.client.post(reverse('basetheme_bootstrap:login'), data, follow=False)
+        self.assertEqual(response.status_code, 302)
+        self.assertTrue(response.wsgi_request.user.is_authenticated)
+
+    def test_works(self):
+        data = {
+            'username': self.user.username,
+            'email': self.user.email,
+            'password': "NOT IT",
+        }
+        response = self.client.post(reverse('basetheme_bootstrap:login'), data, follow=False)
+        self.assertEqual(response.status_code, 200)
+        self.assertFalse(response.wsgi_request.user.is_authenticated)
+
+        for i in range(10):
+            self.client.post(reverse('basetheme_bootstrap:login'), data, follow=False)
+
+        response = self.client.post(reverse('basetheme_bootstrap:login'), data, follow=False)
+        self.assertFalse(response.wsgi_request.user.is_authenticated)
+
+
+@override_settings(
+    BASETHEME_BOOTSTRAP_FAILED_LOGIN_EXPONENTIAL_BACKOFF_GRADIENT=1,
+)
+class ExponentialBackoffFailedLoginAfterDelayTests(TestCase):
+    def setUp(self):
+        cache.clear()
+        self.user_pwd = "eil2guj4cuSho2Vai3hu"
+        self.user = get_user_model().objects.create(
+            username="a",
+            email="a@a.a",
+        )
+        self.user.set_password(self.user_pwd)
+        self.user.save()
+
+    def test_works(self):
+        data = {
+            'username': self.user.username,
+            'email': self.user.email,
+            'password': "NOT IT",
+        }
+
+        for i in range(10):
+            response = self.client.post(reverse('basetheme_bootstrap:login'), data, follow=False)
+            self.assertFalse(response.wsgi_request.user.is_authenticated)
+
+        sleep(2)
+
+        response = self.client.post(
+            reverse('basetheme_bootstrap:login'),
+            data={
+                'username': self.user.username,
+                'email': self.user.email,
+                'password': self.user_pwd,
+            },
+            follow=False,
+        )
+        self.assertTrue(response.wsgi_request.user.is_authenticated)
diff --git a/setup.cfg b/setup.cfg
index 4e0e67cd1d9519ca903795e6972c88bee1c2dd69..e46a5c31edf73796cc87bf8c3673431e9b99db90 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = django-basetheme-bootstrap
-version = 1.7.0
+version = 1.8.0
 description = Django Basetheme Bootstrap
 long_description = file: README.rst
 url = https://gitlab.pasteur.fr/bbrancot/django-basetheme-bootstrap