Repository: duo-labs/py_webauthn

Issue:
Update response structures for new L3 draft JSON serialization 
WebAuthn L3 will introduce new JSON serialization methods and structs that this project can preemptively conform to. See the following links for more info:

- https://w3c.github.io/webauthn/#dom-publickeycredential-tojson
- https://w3c.github.io/webauthn/#sctn-parseCreationOptionsFromJSON
- https://w3c.github.io/webauthn/#sctn-parseRequestOptionsFromJSON

Discussion:

Patch:
diff --git a/examples/registration.py b/examples/registration.py
index 539c066..8f94ea4 100644
--- a/examples/registration.py
+++ b/examples/registration.py
@@ -62,12 +62,12 @@ registration_verification = verify_registration_response(
         "rawId": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
         "response": {
             "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVkBZ0mWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjRQAAAAAAAAAAAAAAAAAAAAAAAAAAACBmggo_UlC8p2tiPVtNQ8nZ5NSxst4WS_5fnElA2viTq6QBAwM5AQAgWQEA31dtHqc70D_h7XHQ6V_nBs3Tscu91kBL7FOw56_VFiaKYRH6Z4KLr4J0S12hFJ_3fBxpKfxyMfK66ZMeAVbOl_wemY4S5Xs4yHSWy21Xm_dgWhLJjZ9R1tjfV49kDPHB_ssdvP7wo3_NmoUPYMgK-edgZ_ehttp_I6hUUCnVaTvn_m76b2j9yEPReSwl-wlGsabYG6INUhTuhSOqG-UpVVQdNJVV7GmIPHCA2cQpJBDZBohT4MBGme_feUgm4sgqVCWzKk6CzIKIz5AIVnspLbu05SulAVnSTB3NxTwCLNJR_9v9oSkvphiNbmQBVQH1tV_psyi9HM1Jtj9VJVKMeyFDAQAB",
-            "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQ2VUV29nbWcwY2NodWlZdUZydjhEWFhkTVpTSVFSVlpKT2dhX3hheVZWRWNCajBDdzN5NzN5aEQ0RmtHU2UtUnJQNmhQSkpBSW0zTFZpZW40aFhFTGciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9"
+            "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQ2VUV29nbWcwY2NodWlZdUZydjhEWFhkTVpTSVFSVlpKT2dhX3hheVZWRWNCajBDdzN5NzN5aEQ0RmtHU2UtUnJQNmhQSkpBSW0zTFZpZW40aFhFTGciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9",
+            "transports": ["internal"]
         },
         "type": "public-key",
         "clientExtensionResults": {},
-        "authenticatorAttachment": "platform",
-        "transports": ["internal"]
+        "authenticatorAttachment": "platform"
     }"""
     ),
     expected_challenge=base64url_to_bytes(
diff --git a/webauthn/helpers/structs.py b/webauthn/helpers/structs.py
index 546a7f7..5aaac61 100644
--- a/webauthn/helpers/structs.py
+++ b/webauthn/helpers/structs.py
@@ -359,12 +359,15 @@ class AuthenticatorAttestationResponse(WebAuthnBaseModel):
     Attributes:
         `client_data_json`: Information the authenticator collects about the client/browser it communicates with
         `attestation_object`: Encoded information about an attestation
+        (optional) `transports`: The authenticator's supported methods of communication with a client/browser
 
     https://www.w3.org/TR/webauthn-2/#authenticatorattestationresponse
     """
 
     client_data_json: bytes
     attestation_object: bytes
+    # Optional in L2, but becomes required in L3. Play it safe until L3 becomes Recommendation
+    transports: Optional[List[AuthenticatorTransport]] = None
 
 
 class RegistrationCredential(WebAuthnBaseModel):
@@ -375,7 +378,6 @@ class RegistrationCredential(WebAuthnBaseModel):
         `raw_id`: A byte sequence representing the credential's unique identifier
         `response`: The authenticator's attesation data
         `type`: The literal string `"public-key"`
-        (optional) `transports`: The authenticator's supported methods of communication with a client/browser
 
     https://www.w3.org/TR/webauthn-2/#publickeycredential
     """
@@ -384,7 +386,6 @@ class RegistrationCredential(WebAuthnBaseModel):
     raw_id: bytes
     response: AuthenticatorAttestationResponse
     authenticator_attachment: Optional[AuthenticatorAttachment] = None
-    transports: Optional[List[AuthenticatorTransport]] = None
     type: Literal[
         PublicKeyCredentialType.PUBLIC_KEY
     ] = PublicKeyCredentialType.PUBLIC_KEY


Test patch:
diff --git a/tests/test_structs.py b/tests/test_structs.py
new file mode 100644
index 0000000..5b8787d
--- /dev/null
+++ b/tests/test_structs.py
@@ -0,0 +1,38 @@
+from unittest import TestCase
+
+from webauthn.helpers.structs import (
+    RegistrationCredential,
+    AuthenticatorTransport,
+    AuthenticatorAttachment,
+)
+
+
+class TestStructsRegistrationCredential(TestCase):
+    def test_registration_credential_parse_raw(self):
+        """
+        Check that we can properly parse some values that aren't really here-or-there for response
+        verification, but can still be useful to RP's to fine-tune the WebAuthn experience.
+        """
+        parsed = RegistrationCredential.parse_raw(
+            """{
+                "id": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
+                "rawId": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
+                "response": {
+                    "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVkBZ0mWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjRQAAAAAAAAAAAAAAAAAAAAAAAAAAACBmggo_UlC8p2tiPVtNQ8nZ5NSxst4WS_5fnElA2viTq6QBAwM5AQAgWQEA31dtHqc70D_h7XHQ6V_nBs3Tscu91kBL7FOw56_VFiaKYRH6Z4KLr4J0S12hFJ_3fBxpKfxyMfK66ZMeAVbOl_wemY4S5Xs4yHSWy21Xm_dgWhLJjZ9R1tjfV49kDPHB_ssdvP7wo3_NmoUPYMgK-edgZ_ehttp_I6hUUCnVaTvn_m76b2j9yEPReSwl-wlGsabYG6INUhTuhSOqG-UpVVQdNJVV7GmIPHCA2cQpJBDZBohT4MBGme_feUgm4sgqVCWzKk6CzIKIz5AIVnspLbu05SulAVnSTB3NxTwCLNJR_9v9oSkvphiNbmQBVQH1tV_psyi9HM1Jtj9VJVKMeyFDAQAB",
+                    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQ2VUV29nbWcwY2NodWlZdUZydjhEWFhkTVpTSVFSVlpKT2dhX3hheVZWRWNCajBDdzN5NzN5aEQ0RmtHU2UtUnJQNmhQSkpBSW0zTFZpZW40aFhFTGciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9",
+                    "transports": ["internal", "hybrid"]
+                },
+                "type": "public-key",
+                "clientExtensionResults": {},
+                "authenticatorAttachment": "platform"
+            }"""
+        )
+
+        self.assertEqual(
+            parsed.response.transports,
+            [
+                AuthenticatorTransport.INTERNAL,
+                AuthenticatorTransport.HYBRID,
+            ],
+        )
+        self.assertEqual(parsed.authenticator_attachment, AuthenticatorAttachment.PLATFORM)