diff --git a/accounts/otp.py b/accounts/otp.py new file mode 100644 index 00000000..585f2b30 --- /dev/null +++ b/accounts/otp.py @@ -0,0 +1,36 @@ +"""The login form used when OTP_ENABLED. + +django-otp's ``OTPAuthenticationForm`` requires every user to present a token +and, since 1.6, to name the device explicitly, so the single-field login page +cannot satisfy it and a user who never enrolled is locked out. This form makes +OTP per user: a user with a confirmed TOTP device must enter a valid code, one +without a device logs in with the password alone. The device is chosen by the +server, so the page needs only the token field and a posted device id (which +django-otp does not check for confirmed status or ownership-by-type) is +ignored. +""" + +from django_otp.forms import OTPAuthenticationForm +from django_otp.plugins.otp_totp.models import TOTPDevice + + +class SingleDeviceOTPForm(OTPAuthenticationForm): + def _confirmed_totp(self, user, for_verify=False): + """The user's oldest confirmed TOTP device, or None. With for_verify + the row is locked (select_for_update) so the token's replay and + throttling counters cannot race; call it only inside clean_otp's + transaction.""" + devices = TOTPDevice.objects.filter(user=user, confirmed=True) + if for_verify: + devices = devices.select_for_update() + return devices.order_by("id").first() + + def _chosen_device(self, user): + # Ignore a posted otp_device: it is verified within clean_otp's + # transaction, where the lock belongs. + return self._confirmed_totp(user, for_verify=True) + + def clean_otp(self, user): + if user is not None and self._confirmed_totp(user) is None: + return # no confirmed device: the password is the only factor + return super().clean_otp(user) diff --git a/accounts/templates/accounts/otp_login.html b/accounts/templates/accounts/otp_login.html index fcd22e96..a11fd4e0 100644 --- a/accounts/templates/accounts/otp_login.html +++ b/accounts/templates/accounts/otp_login.html @@ -55,8 +55,9 @@
- +
+ {% trans "Leave empty if you have not set up two-factor authentication." %}