Repository: pypi/warehouse

Issue:
Ignore whitespace for 2FA
As reported in https://github.com/pypa/warehouse/issues/7584, trying to use a 2FA code that has whitespace currently fails:

> Scenario 1 (I put the 6 digit number with the space)
>
> I get the validation message: Please match the requested format.. I assume this behavior is expected, BUT I don't see any expected format in the page.

We should remove all whitespace from user-submitted 2FA codes. We might also want to improve the error message here to include the expected format.

---

**Good First Issue**: This issue is good for first time contributors. If you've already contributed to Warehouse, work on [another issue without this label](https://github.com/pypa/warehouse/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+-label%3A%22good+first+issue%22) instead. If there is not a corresponding pull request for this issue, it is up for grabs. For directions for getting set up, see our [Getting Started Guide](https://warehouse.pypa.io/development/getting-started/). If you are working on this issue and have questions, feel free to ask them here, [`#pypa-dev` on Freenode](https://webchat.freenode.net/?channels=%23pypa-dev), or the [pypa-dev mailing list](https://groups.google.com/forum/#!forum/pypa-dev).


Discussion:

Resolving patch:
diff --git a/warehouse/accounts/forms.py b/warehouse/accounts/forms.py
--- a/warehouse/accounts/forms.py
+++ b/warehouse/accounts/forms.py
@@ -48,7 +48,7 @@ class TOTPValueMixin:
         validators=[
             wtforms.validators.DataRequired(),
             wtforms.validators.Regexp(
-                rf"^[0-9]{{{TOTP_LENGTH}}}$",
+                rf"^ *([0-9] *){{{TOTP_LENGTH}}}$",
                 message=_(
                     "TOTP code must be ${totp_length} digits.",
                     mapping={"totp_length": TOTP_LENGTH},
@@ -296,7 +296,7 @@ def __init__(self, *args, user_id, user_service, **kwargs):
 
 class TOTPAuthenticationForm(TOTPValueMixin, _TwoFactorAuthenticationForm):
     def validate_totp_value(self, field):
-        totp_value = field.data.encode("utf8")
+        totp_value = field.data.replace(" ", "").encode("utf8")
 
         if not self.user_service.check_totp_value(self.user_id, totp_value):
             raise wtforms.validators.ValidationError(_("Invalid TOTP code."))


Test patch:
diff --git a/tests/unit/accounts/test_forms.py b/tests/unit/accounts/test_forms.py
--- a/tests/unit/accounts/test_forms.py
+++ b/tests/unit/accounts/test_forms.py
@@ -596,6 +596,14 @@ def test_totp_secret_exists(self, pyramid_config):
         assert not form.validate()
         assert str(form.totp_value.errors.pop()) == "TOTP code must be 6 digits."
 
+        form = forms.TOTPAuthenticationForm(
+            data={"totp_value": "1 2 3 4 5 6 7"},
+            user_id=pretend.stub(),
+            user_service=pretend.stub(check_totp_value=lambda *a: True),
+        )
+        assert not form.validate()
+        assert str(form.totp_value.errors.pop()) == "TOTP code must be 6 digits."
+
         form = forms.TOTPAuthenticationForm(
             data={"totp_value": "123456"},
             user_id=pretend.stub(),
@@ -611,6 +619,20 @@ def test_totp_secret_exists(self, pyramid_config):
         )
         assert form.validate()
 
+        form = forms.TOTPAuthenticationForm(
+            data={"totp_value": " 1 2 3 4  5 6 "},
+            user_id=pretend.stub(),
+            user_service=pretend.stub(check_totp_value=lambda *a: True),
+        )
+        assert form.validate()
+
+        form = forms.TOTPAuthenticationForm(
+            data={"totp_value": "123 456"},
+            user_id=pretend.stub(),
+            user_service=pretend.stub(check_totp_value=lambda *a: True),
+        )
+        assert form.validate()
+
 
 class TestWebAuthnAuthenticationForm:
     def test_creation(self):