6
6
7
7
from binascii import unhexlify
8
8
from time import time
9
- from typing import cast
9
+ from typing import TYPE_CHECKING , cast
10
10
11
11
from crispy_forms .helper import FormHelper
12
12
from crispy_forms .layout import HTML , Div , Field , Fieldset , Layout , Submit
20
20
from django .utils .html import escape
21
21
from django .utils .translation import activate , gettext , gettext_lazy , ngettext , pgettext
22
22
from django_otp .forms import OTPTokenForm as DjangoOTPTokenForm
23
+ from django_otp .forms import otp_verification_failed
23
24
from django_otp .oath import totp
25
+ from django_otp .plugins .otp_static .models import StaticDevice
24
26
from django_otp .plugins .otp_totp .models import TOTPDevice
25
27
26
28
from weblate .accounts .auth import try_get_user
57
59
from weblate .utils .ratelimit import check_rate_limit , get_rate_setting , reset_rate_limit
58
60
from weblate .utils .validators import validate_fullname
59
61
62
+ if TYPE_CHECKING :
63
+ from django_otp .models import Device
64
+
60
65
61
66
class UniqueEmailMixin (forms .Form ):
62
67
validate_unique_mail = False
@@ -1043,6 +1048,7 @@ class OTPTokenForm(DjangoOTPTokenForm):
1043
1048
"Recovery token can be used just once, mark your token as used after using it."
1044
1049
),
1045
1050
)
1051
+ device_class : type [Device ] = StaticDevice
1046
1052
1047
1053
def __init__ (self , user , request = None , * args , ** kwargs ):
1048
1054
super ().__init__ (user , request , * args , ** kwargs )
@@ -1066,13 +1072,41 @@ def device_choices(user): # noqa: ARG004
1066
1072
# Also this is incompatible with WebAuthn
1067
1073
return []
1068
1074
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
+
1069
1102
1070
1103
class TOTPTokenForm (OTPTokenForm ):
1071
1104
otp_token = forms .IntegerField (
1072
1105
label = gettext_lazy ("Enter the code from the app" ),
1073
1106
min_value = 0 ,
1074
1107
max_value = 999999 ,
1075
1108
)
1109
+ device_class : type [Device ] = TOTPDevice
1076
1110
1077
1111
def __init__ (self , user , request = None , * args , ** kwargs ):
1078
1112
super ().__init__ (user , request , * args , ** kwargs )
0 commit comments