Compare commits

...

1 Commits

Author SHA1 Message Date
Raphael Michel
45bff138d7 Send security notification when recovery code is used or created by admin
"Where to store recovery codes" is one of these problems there is no
right answer to, so many people store them in a less-than-optimal place.
If that's the reality we live in, this PR adds at least a little
security so one notices when they get used :)
2025-12-12 15:30:22 +01:00
3 changed files with 24 additions and 0 deletions

View File

@@ -57,6 +57,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView
from django_otp import match_token
from django_otp.plugins.otp_static.models import StaticDevice
from webauthn.helpers import generate_challenge
from pretix.base.auth import get_auth_backends
@@ -538,6 +539,10 @@ class Login2FAView(TemplateView):
break
else:
valid = match_token(self.user, token)
if isinstance(valid, StaticDevice):
self.user.send_security_notice([
_("A recovery code for two-factor authentification was used to log in.")
])
if valid:
logger.info(f"Backend login successful for user {self.user.pk} with 2FA.")

View File

@@ -165,6 +165,9 @@ class UserEmergencyTokenView(AdministratorPermissionRequiredMixin, RecentAuthent
d, __ = StaticDevice.objects.get_or_create(user=self.object, name='emergency')
token = d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
self.object.log_action('pretix.user.settings.2fa.emergency', user=self.request.user)
self.object.send_security_notice([
_('A two-factor emergency code has been generated by a system administrator.')
])
messages.success(request, _(
'The emergency token for this user is "{token}". It can only be used once. Please make sure to transmit '

View File

@@ -42,8 +42,10 @@ from django.contrib.auth.tokens import (
)
from django.core import mail as djmail
from django.test import RequestFactory, TestCase, override_settings
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django_otp.oath import TOTP
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
from webauthn.authentication.verify_authentication_response import (
VerifiedAuthentication,
@@ -492,6 +494,20 @@ class Login2FAFormTest(TestCase):
m.undo()
def test_recovery_code_valid(self):
djmail.outbox = []
d, __ = StaticDevice.objects.get_or_create(user=self.user, name='emergency')
token = d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
response = self.client.get('/control/login/2fa')
assert 'token' in response.content.decode()
response = self.client.post('/control/login/2fa', {
'token': token.token,
})
self.assertEqual(response.status_code, 302)
self.assertIn('/control/', response['Location'])
assert "recovery code" in djmail.outbox[0].body
class FakeRedis(object):
def get_redis_connection(self, connection_string):