Skip to content

Commit b71d340

Browse files
committedAug 30, 2024·
fix(auth): manually filter OTP devices when authenticating
We know the class we want to use, so there is no need to iterate over other class (which might fail due to issues like Stormbase/django-otp-webauthn#22). Fixes #12369 Fixes WEBLATE-DYJ
1 parent 05477ef commit b71d340

File tree

3 files changed

+37
-2
lines changed

3 files changed

+37
-2
lines changed
 

‎docs/changes.rst

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Not yet released.
1616
* Fixed authentication using some third-party providers such as Azure.
1717
* Support for formal and informal Portuguese in :ref:`mt-deepl`.
1818
* QR code for TOTP is now black/white even in dark mode.
19+
* Fixed TOTP authentication when WebAuthn is also configured for the user.
1920

2021
**Compatibility**
2122

‎pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ dependencies = [
4545
"django-crispy-forms>=2.1,<2.4",
4646
"django-filter>=23.4,<24.4",
4747
"django-redis>=5.4.0,<6.0",
48-
"django-otp>=1.5.1,<2.0",
48+
"django-otp>=1.5.2,<2.0",
4949
"django-otp-webauthn>=0.3.0,<0.4",
5050
"Django[argon2]>=5.0,<5.2",
5151
"djangorestframework>=3.15.0,<3.16",

‎weblate/accounts/forms.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from binascii import unhexlify
88
from time import time
9-
from typing import cast
9+
from typing import TYPE_CHECKING, cast
1010

1111
from crispy_forms.helper import FormHelper
1212
from crispy_forms.layout import HTML, Div, Field, Fieldset, Layout, Submit
@@ -20,7 +20,9 @@
2020
from django.utils.html import escape
2121
from django.utils.translation import activate, gettext, gettext_lazy, ngettext, pgettext
2222
from django_otp.forms import OTPTokenForm as DjangoOTPTokenForm
23+
from django_otp.forms import otp_verification_failed
2324
from django_otp.oath import totp
25+
from django_otp.plugins.otp_static.models import StaticDevice
2426
from django_otp.plugins.otp_totp.models import TOTPDevice
2527

2628
from weblate.accounts.auth import try_get_user
@@ -57,6 +59,9 @@
5759
from weblate.utils.ratelimit import check_rate_limit, get_rate_setting, reset_rate_limit
5860
from weblate.utils.validators import validate_fullname
5961

62+
if TYPE_CHECKING:
63+
from django_otp.models import Device
64+
6065

6166
class UniqueEmailMixin(forms.Form):
6267
validate_unique_mail = False
@@ -1043,6 +1048,7 @@ class OTPTokenForm(DjangoOTPTokenForm):
10431048
"Recovery token can be used just once, mark your token as used after using it."
10441049
),
10451050
)
1051+
device_class: type[Device] = StaticDevice
10461052

10471053
def __init__(self, user, request=None, *args, **kwargs):
10481054
super().__init__(user, request, *args, **kwargs)
@@ -1066,13 +1072,41 @@ def device_choices(user): # noqa: ARG004
10661072
# Also this is incompatible with WebAuthn
10671073
return []
10681074

1075+
def _verify_token(
1076+
self, user: User, token: str, device: Device | None = None
1077+
) -> Device:
1078+
if device is not None:
1079+
return super()._verify_token(user, token, device)
1080+
1081+
# We want to list only correct device classes, not all as django-otp does in match_token
1082+
with transaction.atomic():
1083+
device_set = self.device_class.objects.devices_for_user(
1084+
user, confirmed=True
1085+
)
1086+
result = None
1087+
for current_device in device_set.select_for_update():
1088+
if current_device.verify_token(token):
1089+
result = current_device
1090+
break
1091+
1092+
if result is None:
1093+
otp_verification_failed.send(
1094+
sender=self.__class__,
1095+
user=user,
1096+
)
1097+
raise forms.ValidationError(
1098+
self.otp_error_messages["invalid_token"], code="invalid_token"
1099+
)
1100+
return result
1101+
10691102

10701103
class TOTPTokenForm(OTPTokenForm):
10711104
otp_token = forms.IntegerField(
10721105
label=gettext_lazy("Enter the code from the app"),
10731106
min_value=0,
10741107
max_value=999999,
10751108
)
1109+
device_class: type[Device] = TOTPDevice
10761110

10771111
def __init__(self, user, request=None, *args, **kwargs):
10781112
super().__init__(user, request, *args, **kwargs)

0 commit comments

Comments
 (0)