- commit
- 8dfa12427d678b37c0189ec821515379fb0b7cc1
- parent
- 2610ba11323ad202849483a7b8afba4ffbf4d5e8
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2025-08-13 10:25
add setting MAX_KEYS_PER_ACCOUNT
Diffstat
| M | mfa/settings.py | 9 | +++++++++ |
| M | mfa/templates/mfa/mfakey_list.html | 13 | ++++++++++--- |
| M | mfa/views.py | 14 | ++++++++++++++ |
| M | tests/tests.py | 22 | ++++++++++++++++++++++ |
4 files changed, 55 insertions, 3 deletions
diff --git a/mfa/settings.py b/mfa/settings.py
@@ -17,3 +17,12 @@ TOTP_VALID_WINDOW = getattr(settings, 'MFA_TOTP_VALID_WINDOW', 0) 17 17 # `user_verification` parameter passed to python-fido2 18 18 # See https://www.w3.org/TR/webauthn/#enum-userVerificationRequirement 19 19 FIDO2_USER_VERIFICATION = getattr(settings, 'MFA_FIDO2_USER_VERIFICATION', None) -1 20 -1 21 # An account might end up having a large number of keys -1 22 # if it is shared by multiple users or if lost keys are not -1 23 # deleted. Both are potential security issues, so this is -1 24 # restricted to a reasonable (but small) number by default. -1 25 # -1 26 # This setting only applies when adding new keys. -1 27 # To allow an arbitrary number of keys, set this to `None`. -1 28 MAX_KEYS_PER_ACCOUNT = getattr(settings, 'MFA_MAX_KEYS_PER_ACCOUNT', 3)
diff --git a/mfa/templates/mfa/mfakey_list.html b/mfa/templates/mfa/mfakey_list.html
@@ -18,6 +18,13 @@ 18 18 {% endfor %} 19 19 </ul> 20 2021 -1 <a href="{% url 'mfa:create' 'TOTP' %}">Create TOTP key</a>22 -1 <a href="{% url 'mfa:create' 'FIDO2' %}">Create FIDO2 key</a>23 -1 <a href="{% url 'mfa:create' 'recovery' %}">Create recovery code</a>-1 21 {% if max_keys and user.mfakey_set.count >= max_keys %} -1 22 <p> -1 23 You cannot have more than {{ max_keys }} keys. -1 24 Please delete one of your existing keys before adding a new one. -1 25 </p> -1 26 {% else %} -1 27 <a href="{% url 'mfa:create' 'TOTP' %}">Create TOTP key</a> -1 28 <a href="{% url 'mfa:create' 'FIDO2' %}">Create FIDO2 key</a> -1 29 <a href="{% url 'mfa:create' 'recovery' %}">Create recovery code</a> -1 30 {% endif %}
diff --git a/mfa/views.py b/mfa/views.py
@@ -10,6 +10,7 @@ from django.shortcuts import redirect 10 10 from django.urls import reverse 11 11 from django.utils.decorators import method_decorator 12 12 from django.utils.functional import cached_property -1 13 from django.utils.text import format_lazy 13 14 from django.utils.translation import gettext_lazy as _ 14 15 from django.views.generic import DeleteView 15 16 from django.views.generic import ListView @@ -49,6 +50,11 @@ class MFAListView(LoginRequiredMixin, ListView): 49 50 def get_queryset(self): 50 51 return super().get_queryset().filter(user=self.request.user) 51 52 -1 53 def get_context_data(self, **kwargs): -1 54 context = super().get_context_data(**kwargs) -1 55 context['max_keys'] = settings.MAX_KEYS_PER_ACCOUNT -1 56 return context -1 57 52 58 53 59 class MFADeleteView(LoginRequiredMixin, DeleteView): 54 60 model = MFAKey @@ -76,6 +82,14 @@ class MFACreateView(LoginRequiredMixin, MFAFormView): 76 82 return self.method.register_complete(self.challenge[1], code) 77 83 78 84 def form_valid(self, form): -1 85 if settings.MAX_KEYS_PER_ACCOUNT: -1 86 count = self.request.user.mfakey_set.count() -1 87 if count >= settings.MAX_KEYS_PER_ACCOUNT: -1 88 form.add_error(None, format_lazy(_( -1 89 'You cannot have more than {} keys. Please delete ' -1 90 'one of your existing keys before adding a new one.' -1 91 ), settings.MAX_KEYS_PER_ACCOUNT)) -1 92 return self.form_invalid(form) 79 93 MFAKey.objects.create( 80 94 user=self.request.user, 81 95 method=self.method.name,
diff --git a/tests/tests.py b/tests/tests.py
@@ -166,6 +166,28 @@ class TOTPCreateViewTest(MFATestCase): 166 166 }) 167 167 self.assertEqual(MFAKey.objects.count(), 1) 168 168 -1 169 def test_max_keys(self): -1 170 for i in range(3): -1 171 self.user.mfakey_set.create( -1 172 method='recovery', -1 173 name=f'test-{i}', -1 174 secret='dummy', -1 175 ) -1 176 -1 177 self.client.force_login(self.user) -1 178 -1 179 res = self.client.get('/mfa/create/TOTP/') -1 180 self.assertEqual(res.status_code, 200) -1 181 totp = pyotp.TOTP(res.context['mfa_data']['secret']) -1 182 -1 183 with self.settings(MFA_MAX_KEYS_PER_ACCOUNT=3): -1 184 res = self.client.post('/mfa/create/TOTP/', { -1 185 'name': 'test', -1 186 'code': totp.now() -1 187 }) -1 188 self.assertEqual(res.status_code, 200) -1 189 self.assertEqual(MFAKey.objects.count(), 3) -1 190 169 191 170 192 class FIDO2Test(MFATestCase): 171 193 # I have no clue how to simulate a FIDO2 authenticator,