Repository: duo-labs/py_webauthn

Issue:
Pydantic/bytes validation doesn't accept Python memoryview
The latest version going back to pydantic with a custom 'bytes' validation class still throws an error during runtime validation in the case of peewee+postresql.

In this case - a python memoryview type is being returned. That should also be 'ok' as being bytes. It seems like it would be very simple to extend the BytesLike validator to handle this case.

Note: the original mongodb issue - which subclassed 'bytes' works fine....

Discussion:

Patch:
diff --git a/webauthn/helpers/structs.py b/webauthn/helpers/structs.py
index 9ae33bc..74274ab 100644
--- a/webauthn/helpers/structs.py
+++ b/webauthn/helpers/structs.py
@@ -44,6 +44,7 @@ class BytesLike(bytes):
     - https://github.com/duo-labs/py_webauthn/issues/110
     - https://github.com/duo-labs/py_webauthn/issues/113
     """
+
     @classmethod
     def __get_validators__(cls):
         yield cls.validate
@@ -52,6 +53,8 @@ class BytesLike(bytes):
     def validate(cls, v):
         if isinstance(v, bytes):
             return v
+        elif isinstance(v, memoryview):
+            return v.tobytes()
         else:
             return strict_bytes_validator(v)
 


Test patch:
diff --git a/tests/test_bytes_subclass_support.py b/tests/test_bytes_subclass_support.py
index c6c9783..3fe50d6 100644
--- a/tests/test_bytes_subclass_support.py
+++ b/tests/test_bytes_subclass_support.py
@@ -7,19 +7,19 @@ from webauthn.helpers.structs import (
 )
 
 
-class CustomBytes(bytes):
-    def __new__(cls, data: str):
-        data_bytes = base64url_to_bytes(data)
-        self = bytes.__new__(cls, memoryview(data_bytes).tobytes())
-        return self
-
-
 class TestWebAuthnBytesSubclassSupport(TestCase):
     def test_handles_bytes_subclasses(self) -> None:
         """
         Ensure the library can support being used in projects that might work with values that are
         subclasses of `bytes`. Let's embrace Python's duck-typing, not shy away from it
         """
+
+        class CustomBytes(bytes):
+            def __new__(cls, data: str):
+                data_bytes = base64url_to_bytes(data)
+                self = bytes.__new__(cls, memoryview(data_bytes).tobytes())
+                return self
+
         verification = verify_authentication_response(
             credential=AuthenticationCredential(
                 id="fq9Nj0nS24B5y6Pkw_h3-9GEAEA3-0LBPxE2zvTdLjDqtSeCSNYFe9VMRueSpAZxT3YDc6L1lWXdQNwI-sVNYrefEcRR1Nsb_0jpHE955WEtFud2xxZg3MvoLMxHLet63i5tajd1fHtP7I-00D6cehM8ZWlLp2T3s9lfZgVIFcA",
@@ -50,3 +50,43 @@ class TestWebAuthnBytesSubclassSupport(TestCase):
         )
 
         assert verification.new_sign_count == 7
+
+    def test_handles_memoryviews(self) -> None:
+        """
+        Ensure support for libraries that leverage memoryviews
+        """
+
+        def base64url_to_memoryview(data: str) -> memoryview:
+            data_bytes = base64url_to_bytes(data)
+            return memoryview(data_bytes)
+
+        verification = verify_authentication_response(
+            credential=AuthenticationCredential(
+                id="fq9Nj0nS24B5y6Pkw_h3-9GEAEA3-0LBPxE2zvTdLjDqtSeCSNYFe9VMRueSpAZxT3YDc6L1lWXdQNwI-sVNYrefEcRR1Nsb_0jpHE955WEtFud2xxZg3MvoLMxHLet63i5tajd1fHtP7I-00D6cehM8ZWlLp2T3s9lfZgVIFcA",
+                raw_id=base64url_to_memoryview(
+                    "fq9Nj0nS24B5y6Pkw_h3-9GEAEA3-0LBPxE2zvTdLjDqtSeCSNYFe9VMRueSpAZxT3YDc6L1lWXdQNwI-sVNYrefEcRR1Nsb_0jpHE955WEtFud2xxZg3MvoLMxHLet63i5tajd1fHtP7I-00D6cehM8ZWlLp2T3s9lfZgVIFcA"
+                ),
+                response=AuthenticatorAssertionResponse(
+                    authenticator_data=base64url_to_memoryview(
+                        "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAABw"
+                    ),
+                    client_data_json=base64url_to_memoryview(
+                        "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZVo0ZWVBM080ank1Rkl6cURhU0o2SkROR3UwYkJjNXpJMURqUV9rTHNvMVdOcWtHNms1bUNZZjFkdFFoVlVpQldaV2xaa3pSNU1GZWVXQ3BKUlVOWHciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9"
+                    ),
+                    signature=base64url_to_memoryview(
+                        "RRWV8mYDRvK7YdQgdtZD4pJ2dh1D_IWZ_D6jsZo6FHJBoenbj0CVT5nA20vUzlRhN4R6dOEUHmUwP1F8eRBhBg"
+                    ),
+                ),
+            ),
+            expected_challenge=base64url_to_memoryview(
+                "eZ4eeA3O4jy5FIzqDaSJ6JDNGu0bBc5zI1DjQ_kLso1WNqkG6k5mCYf1dtQhVUiBWZWlZkzR5MFeeWCpJRUNXw"
+            ),
+            expected_rp_id="localhost",
+            expected_origin="http://localhost:5000",
+            credential_public_key=base64url_to_memoryview(
+                "pAEBAycgBiFYIMz6_SUFLiDid2Yhlq0YboyJ-CDrIrNpkPUGmJp4D3Dp"
+            ),
+            credential_current_sign_count=3,
+        )
+
+        assert verification.new_sign_count == 7