diff --git a/codeforlife/user/signals/__init__.py b/codeforlife/user/signals/__init__.py index 29cc9d7d..b839e40b 100644 --- a/codeforlife/user/signals/__init__.py +++ b/codeforlife/user/signals/__init__.py @@ -4,5 +4,6 @@ """ # NOTE: Need to import signals so they are discoverable by Django. +from .auth_factor import auth_factor__post_delete from .teacher import teacher_receiver from .user import user_receiver diff --git a/codeforlife/user/signals/auth_factor.py b/codeforlife/user/signals/auth_factor.py new file mode 100644 index 00000000..03457784 --- /dev/null +++ b/codeforlife/user/signals/auth_factor.py @@ -0,0 +1,25 @@ +""" +© Ocado Group +Created on 17/01/2025 at 15:55:22(+00:00). +""" + +import pyotp +from django.db.models import signals +from django.dispatch import receiver + +from ..models import AuthFactor + +# pylint: disable=missing-function-docstring +# pylint: disable=unused-argument + + +@receiver(signals.post_delete, sender=AuthFactor) +def auth_factor__post_delete(sender, instance: AuthFactor, **kwargs): + # Create new secret to ensure secrets are not recycled. + if instance.type == AuthFactor.Type.OTP: + otp_secret = instance.user.userprofile.otp_secret + # Ensure the randomly generated new secret is different to the previous. + while otp_secret == instance.user.userprofile.otp_secret: + instance.user.userprofile.otp_secret = pyotp.random_base32() + + instance.user.userprofile.save(update_fields=["otp_secret"]) diff --git a/codeforlife/user/signals/auth_factor_test.py b/codeforlife/user/signals/auth_factor_test.py new file mode 100644 index 00000000..efc763ee --- /dev/null +++ b/codeforlife/user/signals/auth_factor_test.py @@ -0,0 +1,28 @@ +""" +© Ocado Group +Created on 17/01/2025 at 16:04:46(+00:00). +""" + +from django.test import TestCase + +from ..models import AuthFactor + + +# pylint: disable-next=missing-class-docstring +class TestAuthFactor(TestCase): + fixtures = ["school_2"] + + def test_post_delete(self): + """Deleting an otp-auth-factor assigns a new otp-secret to its user.""" + auth_factor = AuthFactor.objects.filter( + type=AuthFactor.Type.OTP + ).first() + assert auth_factor + + userprofile = auth_factor.user.userprofile + otp_secret = userprofile.otp_secret + + auth_factor.delete() + + userprofile.refresh_from_db() + assert otp_secret != userprofile.otp_secret