Skip to content

Commit

Permalink
Fix dataclass constructors with webauthn_json_mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
dainnilsson committed Sep 23, 2024
1 parent fb84b73 commit 28b07fb
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 17 deletions.
27 changes: 19 additions & 8 deletions fido2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@
)
from .cose import ES256
from .rpid import verify_rp_id
from .utils import sha256
from .utils import sha256, _DataClassMapping
from enum import IntEnum, unique
from urllib.parse import urlparse
from dataclasses import replace
from dataclasses import replace, asdict
from threading import Timer, Event
from typing import (
Type,
Expand All @@ -69,6 +69,17 @@
logger = logging.getLogger(__name__)


def _as_cbor(data):
if data is None:
return None
if isinstance(data, Sequence):
return [_as_cbor(d) for d in data]
if isinstance(data, _DataClassMapping):
# Remove empty values and do not serialize value
return {k: v for k, v in asdict(data).items() if v is not None} # type: ignore
return data


class ClientError(Exception):
@unique
class ERR(IntEnum):
Expand Down Expand Up @@ -337,7 +348,7 @@ def do_make_credential(
if (
rk
or user_verification == UserVerificationRequirement.REQUIRED
or ES256.ALGORITHM not in [p.alg for p in key_params]
or ES256.ALGORITHM not in [p["alg"] for p in key_params]
or enterprise_attestation
):
raise CtapError(CtapError.ERR.UNSUPPORTED_OPTION)
Expand Down Expand Up @@ -803,10 +814,10 @@ def make_credential(
try:
return self._backend.do_make_credential(
client_data,
rp,
options.user,
options.pub_key_cred_params,
options.exclude_credentials,
_as_cbor(rp),
_as_cbor(options.user),
_as_cbor(options.pub_key_cred_params),
_as_cbor(options.exclude_credentials),
options.extensions,
selection.require_resident_key,
selection.user_verification,
Expand Down Expand Up @@ -848,7 +859,7 @@ def get_assertion(
return self._backend.do_get_assertion(
client_data,
options.rp_id,
options.allow_credentials,
_as_cbor(options.allow_credentials),
options.extensions,
options.user_verification,
event,
Expand Down
22 changes: 17 additions & 5 deletions fido2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def _snake2camel(name: str) -> str:
return parts[0] + "".join(p.title() for p in parts[1:])


def _parse_value(t, value):
def _parse_value(t, value, from_dict):
if value is None:
return None

Expand All @@ -181,7 +181,7 @@ def _parse_value(t, value):
# Handle list of values
if issubclass(getattr(t, "__origin__", object), Sequence):
t = t.__args__[0]
return [_parse_value(t, v) for v in value]
return [_parse_value(t, v, from_dict) for v in value]

# Handle Mappings
if issubclass(getattr(t, "__origin__", object), Mapping) and isinstance(
Expand All @@ -196,15 +196,24 @@ def _parse_value(t, value):
except TypeError:
pass

# If called from "from_dict", recurse using the same
if from_dict and hasattr(t, "from_dict"):
return t.from_dict(value)

# Check for subclass of _DataClassMapping
try:
is_dataclass = issubclass(t, _DataClassMapping)
except TypeError:
is_dataclass = False

if is_dataclass:
# Recursively call from_dict for nested _DataClassMappings
return t.from_dict(value)
# Recursively call constructor for nested _DataClassMappings
kwargs = {}
for f in fields(t): # type: ignore
key = t._get_field_key(f)
if key in value:
kwargs[f.name] = value[key]
return t(**kwargs)

# Convert to enum values, other wrappers
return t(value)
Expand All @@ -224,7 +233,7 @@ def __post_init__(self):
if value is None:
continue
try:
value = _parse_value(hints[f.name], value)
value = _parse_value(hints[f.name], value, False)
except (TypeError, KeyError, ValueError):
raise ValueError(
f"Error parsing field {f.name} for {self.__class__.__name__}"
Expand Down Expand Up @@ -275,6 +284,7 @@ def from_dict(cls, data: Optional[Mapping[_T, Any]]):
)

kwargs = {}
hints = get_type_hints(cls)
for f in fields(cls): # type: ignore
key = cls._get_field_key(f)
if key in data:
Expand All @@ -283,6 +293,8 @@ def from_dict(cls, data: Optional[Mapping[_T, Any]]):
deserialize = f.metadata.get("deserialize")
if deserialize:
value = deserialize(value)
else:
value = _parse_value(hints[f.name], value, True)
kwargs[f.name] = value
return cls(**kwargs)

Expand Down
4 changes: 2 additions & 2 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import unittest
from unittest import mock
from fido2 import cbor
from fido2.utils import sha256, websafe_encode
from fido2.utils import sha256
from fido2.hid import CAPABILITY
from fido2.ctap import CtapError
from fido2.ctap1 import RegistrationData
Expand All @@ -51,7 +51,7 @@
)

rp = {"id": "example.com", "name": "Example RP"}
user = {"id": websafe_encode(b"user_id"), "name": "A. User"}
user = {"id": b"user_id", "name": "A. User"}
challenge = b"Y2hhbGxlbmdl"
_INFO_NO_PIN = bytes.fromhex(
"a60182665532465f5632684649444f5f325f3002826375766d6b686d61632d7365637265740350f8a011f38c0a4d15800617111f9edc7d04a462726bf5627570f564706c6174f469636c69656e7450696ef4051904b0068101" # noqa E501
Expand Down
4 changes: 2 additions & 2 deletions tests/test_webauthn.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ def test_creation_options(self):
b"request_challenge",
[{"type": "public-key", "alg": -7}],
10000,
[{"type": "public-key", "id": websafe_encode(b"credential_id")}],
[{"type": "public-key", "id": b"credential_id"}],
{
"authenticatorAttachment": "platform",
"residentKey": "required",
Expand Down Expand Up @@ -290,7 +290,7 @@ def test_creation_options(self):
self.assertIsNone(
PublicKeyCredentialCreationOptions(
{"id": "example.com", "name": "Example"},
{"id": websafe_encode(b"user_id"), "name": "A. User"},
{"id": b"user_id", "name": "A. User"},
b"request_challenge",
[{"type": "public-key", "alg": -7}],
attestation="invalid",
Expand Down

0 comments on commit 28b07fb

Please sign in to comment.