Skip to content

Commit 8755b72

Browse files
author
kobo-bot[bot]
committed
Merge branch 'release/2.025.47'
2 parents 58dfc90 + e36fa29 commit 8755b72

File tree

6 files changed

+49
-43
lines changed

6 files changed

+49
-43
lines changed

kobo/apps/accounts/mfa/forms.py

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,22 @@
11
from allauth.mfa.adapter import get_adapter
22
from allauth.mfa.base.forms import AuthenticateForm, ReauthenticateForm
3-
from allauth.mfa.base.internal.flows import check_rate_limit
4-
from allauth.mfa.models import Authenticator
5-
from django.contrib.auth.hashers import check_password
3+
4+
from .serializers import MfaCodeSerializer
65

76

87
class MfaAuthenticateMixin:
98

109
def clean_code(self):
11-
clear_rl = check_rate_limit(self.user)
1210
code = self.cleaned_data['code']
13-
for auth in Authenticator.objects.filter(user=self.user).exclude(
14-
# WebAuthn cannot validate manual codes.
15-
type=Authenticator.Type.WEBAUTHN
16-
):
17-
if auth.wrap().validate_code(code):
18-
self.authenticator = auth
19-
clear_rl()
20-
return code
21-
if auth.type == Authenticator.Type.RECOVERY_CODES:
22-
hashed_code = self.validate_migrated_codes(code, auth.wrap())
23-
if hashed_code is not None:
24-
self.authenticator = auth
25-
clear_rl()
26-
return code
11+
serializer = MfaCodeSerializer(
12+
data={'code': code}, context={'user': self.user, 'method': 'app'}
13+
)
2714

28-
raise get_adapter().validation_error('incorrect_code')
15+
if serializer.is_valid():
16+
self.authenticator = serializer.authenticator
17+
return code
2918

30-
def validate_migrated_codes(self, input_code, recovery_codes):
31-
codes = recovery_codes._get_migrated_codes()
32-
if codes is None:
33-
return
34-
if not codes[0].startswith('pbkdf2_sha256$'):
35-
return
36-
# if codes are sha256 hashes do the recovery codes logic
37-
for idx, hashed_code in enumerate(codes):
38-
if check_password(input_code, hashed_code):
39-
recovery_codes.validate_code(hashed_code)
40-
return hashed_code
19+
raise get_adapter().validation_error('incorrect_code')
4120

4221

4322
class MfaAuthenticateForm(MfaAuthenticateMixin, AuthenticateForm):

kobo/apps/accounts/mfa/serializers.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from allauth.mfa.totp.internal.auth import TOTP
1+
from allauth.mfa.base.internal.flows import check_rate_limit
2+
from allauth.mfa.models import Authenticator
3+
from django.contrib.auth.hashers import check_password
24
from rest_framework import serializers
35
from rest_framework.exceptions import NotFound, ValidationError
46

@@ -21,9 +23,9 @@ class Meta:
2123
)
2224

2325

24-
class TOTPCodeSerializer(serializers.Serializer):
26+
class MfaCodeSerializer(serializers.Serializer):
2527
"""
26-
Intended to be used for validating TOTP codes
28+
Intended to be used for validating TOTP and recovery codes
2729
"""
2830

2931
code = serializers.CharField()
@@ -38,10 +40,31 @@ def __init__(self, *args, **kwargs):
3840
)
3941
except MfaMethodsWrapper.DoesNotExist:
4042
raise NotFound
41-
self.totp = TOTP(self.mfamethods.totp)
4243

43-
def validate_code(self, value):
44-
if not self.totp.validate_code(value):
45-
raise ValidationError('Invalid TOTP code')
44+
def validate_code(self, code):
45+
clear_rl = check_rate_limit(self.user)
46+
for auth in [self.mfamethods.totp, self.mfamethods.recovery_codes]:
47+
if auth.wrap().validate_code(code):
48+
self.authenticator = auth
49+
clear_rl()
50+
return code
51+
if auth.type == Authenticator.Type.RECOVERY_CODES:
52+
hashed_code = self.validate_migrated_codes(code, auth.wrap())
53+
if hashed_code is not None:
54+
self.authenticator = auth
55+
clear_rl()
56+
return code
4657

47-
return value
58+
raise ValidationError('Invalid code')
59+
60+
def validate_migrated_codes(self, input_code, recovery_codes):
61+
codes = recovery_codes._get_migrated_codes()
62+
if codes is None:
63+
return
64+
if not codes[0].startswith('pbkdf2_sha256$'):
65+
return
66+
# if codes are sha256 hashes do the recovery codes logic
67+
for idx, hashed_code in enumerate(codes):
68+
if check_password(input_code, hashed_code):
69+
recovery_codes.validate_code(hashed_code)
70+
return hashed_code

kobo/apps/accounts/mfa/signals.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ def deactivate_mfa_method_for_user(**kwargs):
1717
mfa_available_to_user = kwargs['instance']
1818
# Deactivate any MFA methods user could have already created
1919
try:
20-
# Use `.get()` + `.save()` (from model `MfaMethod`) instead of
20+
# Use `.get()` + `.save()` (from model `MfaMethodsWrapper`) instead of
2121
# `.update()` to run some logic inside `.save()`. It makes an extra
2222
# query to DB but avoid duplicated code.
2323
mfa_method = MfaMethodsWrapper.objects.get(user=mfa_available_to_user.user)
24-
except MfaMethod.DoesNotExist:
24+
except MfaMethodsWrapper.DoesNotExist:
2525
pass
2626
else:
2727
mfa_method.is_active = False

kobo/apps/accounts/mfa/tests/test_api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from constance.test import override_config
2+
from django.test import override_settings
23
from django.urls import reverse
34
from rest_framework import status
45

@@ -10,6 +11,7 @@
1011
METHOD = 'app'
1112

1213

14+
@override_settings(ACCOUNT_RATE_LIMITS=False)
1315
class MfaApiTestCase(BaseTestCase):
1416

1517
fixtures = ['test_data']

kobo/apps/accounts/mfa/tests/test_migration.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from allauth.account.models import EmailAddress
22
from allauth.mfa.adapter import get_adapter
33
from django.conf import settings
4+
from django.test import override_settings
45
from django.urls import reverse
56
from rest_framework import status
67
from trench.command.replace_mfa_method_backup_codes import (
@@ -13,6 +14,7 @@
1314
from .utils import get_mfa_code_for_user
1415

1516

17+
@override_settings(ACCOUNT_RATE_LIMITS=False)
1618
class MfaMigrationTestCase(BaseTestCase):
1719
"""
1820
Test the migration scripts

kobo/apps/accounts/mfa/views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .flows import activate_totp, deactivate_totp, regenerate_codes
1717
from .models import MfaMethodsWrapper
1818
from .permissions import IsMfaEnabled
19-
from .serializers import TOTPCodeSerializer, UserMfaMethodSerializer
19+
from .serializers import MfaCodeSerializer, UserMfaMethodSerializer
2020

2121

2222
class MfaLoginView(LoginView):
@@ -111,7 +111,7 @@ class MfaMethodDeactivateView(APIView):
111111

112112
@staticmethod
113113
def post(request: Request, method: str) -> Response:
114-
serializer = TOTPCodeSerializer(
114+
serializer = MfaCodeSerializer(
115115
data=request.data, context={'user': request.user, 'method': method}
116116
)
117117
serializer.is_valid(raise_exception=True)
@@ -124,7 +124,7 @@ class MfaMethodRegenerateCodesView(APIView):
124124

125125
@staticmethod
126126
def post(request: Request, method: str) -> Response:
127-
serializer = TOTPCodeSerializer(
127+
serializer = MfaCodeSerializer(
128128
data=request.data, context={'user': request.user, 'method': method}
129129
)
130130
serializer.is_valid(raise_exception=True)

0 commit comments

Comments
 (0)