Skip to content

Commit 4920b3a

Browse files
committed
add setting MAX_KEYS_PER_ACCOUNT
1 parent 2610ba1 commit 4920b3a

File tree

4 files changed

+55
-3
lines changed

4 files changed

+55
-3
lines changed

mfa/settings.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,12 @@
1717
# `user_verification` parameter passed to python-fido2
1818
# See https://www.w3.org/TR/webauthn/#enum-userVerificationRequirement
1919
FIDO2_USER_VERIFICATION = getattr(settings, 'MFA_FIDO2_USER_VERIFICATION', None)
20+
21+
# An account might end up having a large number of keys
22+
# if it is shared by multiple users or if lost keys are not
23+
# deleted. Both are potential security issues, so this is
24+
# restricted to a reasonable (but small) number by default.
25+
#
26+
# This setting only applies when adding new keys.
27+
# To allow an arbitrary number of keys, set this to `None`.
28+
MAX_KEYS_PER_ACCOUNT = getattr(settings, 'MFA_MAX_KEYS_PER_ACCOUNT', 3)

mfa/templates/mfa/mfakey_list.html

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ <h1>{% if object_list %}Login keys configured{% else %}No login keys configured{
1818
{% endfor %}
1919
</ul>
2020

21-
<a href="{% url 'mfa:create' 'TOTP' %}">Create TOTP key</a>
22-
<a href="{% url 'mfa:create' 'FIDO2' %}">Create FIDO2 key</a>
23-
<a href="{% url 'mfa:create' 'recovery' %}">Create recovery code</a>
21+
{% if max_keys and user.mfakey_set.count >= max_keys %}
22+
<p>
23+
You cannot have more than {{ max_keys }} keys.
24+
Please delete one of your existing keys before adding a new one.
25+
</p>
26+
{% else %}
27+
<a href="{% url 'mfa:create' 'TOTP' %}">Create TOTP key</a>
28+
<a href="{% url 'mfa:create' 'FIDO2' %}">Create FIDO2 key</a>
29+
<a href="{% url 'mfa:create' 'recovery' %}">Create recovery code</a>
30+
{% endif %}

mfa/views.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from django.urls import reverse
1111
from django.utils.decorators import method_decorator
1212
from django.utils.functional import cached_property
13+
from django.utils.text import format_lazy
1314
from django.utils.translation import gettext_lazy as _
1415
from django.views.generic import DeleteView
1516
from django.views.generic import ListView
@@ -49,6 +50,11 @@ class MFAListView(LoginRequiredMixin, ListView):
4950
def get_queryset(self):
5051
return super().get_queryset().filter(user=self.request.user)
5152

53+
def get_context_data(self, **kwargs):
54+
context = super().get_context_data(**kwargs)
55+
context['max_keys'] = settings.MAX_KEYS_PER_ACCOUNT
56+
return context
57+
5258

5359
class MFADeleteView(LoginRequiredMixin, DeleteView):
5460
model = MFAKey
@@ -76,6 +82,14 @@ def complete(self, code):
7682
return self.method.register_complete(self.challenge[1], code)
7783

7884
def form_valid(self, form):
85+
if settings.MAX_KEYS_PER_ACCOUNT:
86+
count = self.request.user.mfakey_set.count()
87+
if count >= settings.MAX_KEYS_PER_ACCOUNT:
88+
form.add_error(None, format_lazy(_(
89+
'You cannot have more than {} keys. Please delete '
90+
'one of your existing keys before adding a new one.'
91+
), settings.MAX_KEYS_PER_ACCOUNT))
92+
return self.form_invalid(form)
7993
MFAKey.objects.create(
8094
user=self.request.user,
8195
method=self.method.name,

tests/tests.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,28 @@ def test_keep_challenge_on_validation(self):
166166
})
167167
self.assertEqual(MFAKey.objects.count(), 1)
168168

169+
def test_max_keys(self):
170+
for i in range(3):
171+
self.user.mfakey_set.create(
172+
method='recovery',
173+
name=f'test-{i}',
174+
secret='dummy',
175+
)
176+
177+
self.client.force_login(self.user)
178+
179+
res = self.client.get('/mfa/create/TOTP/')
180+
self.assertEqual(res.status_code, 200)
181+
totp = pyotp.TOTP(res.context['mfa_data']['secret'])
182+
183+
with self.settings(MFA_MAX_KEYS_PER_ACCOUNT=3):
184+
res = self.client.post('/mfa/create/TOTP/', {
185+
'name': 'test',
186+
'code': totp.now()
187+
})
188+
self.assertEqual(res.status_code, 200)
189+
self.assertEqual(MFAKey.objects.count(), 3)
190+
169191

170192
class FIDO2Test(MFATestCase):
171193
# I have no clue how to simulate a FIDO2 authenticator,

0 commit comments

Comments
 (0)