mirror of https://github.com/Yubico/python-fido2
Add support for WebAuthn data class JSON serialization.
See https://github.com/w3c/webauthn/issues/1683 for details.
This commit is contained in:
parent
f7e8c59649
commit
8debc41942
|
@ -0,0 +1,67 @@
|
|||
from typing import Optional
|
||||
|
||||
import warnings
|
||||
|
||||
|
||||
class FeatureNotEnabledError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class _Feature:
|
||||
def __init__(self, name: str, desc: str):
|
||||
self._enabled: Optional[bool] = None
|
||||
self._name = name
|
||||
self._desc = desc
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
self.warn()
|
||||
return self._enabled is True
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, value: bool) -> None:
|
||||
if self._enabled is not None:
|
||||
raise ValueError(
|
||||
f"{self._name} has already been configured with {self._enabled}"
|
||||
)
|
||||
self._enabled = value
|
||||
|
||||
def require(self, state=True) -> None:
|
||||
if self._enabled != state:
|
||||
self.warn()
|
||||
raise FeatureNotEnabledError(
|
||||
f"Usage requires {self._name}.enabled = {state}"
|
||||
)
|
||||
|
||||
def warn(self) -> None:
|
||||
if self._enabled is None:
|
||||
warnings.warn(
|
||||
f"""Deprecated use of {self._name}.
|
||||
|
||||
You are using deprecated functionality which will change in the next major version of
|
||||
python-fido2. You can opt-in to use the new functionality now by adding the following
|
||||
to your code somewhere where it gets executed prior to using the affected functionality:
|
||||
|
||||
import fido2.features
|
||||
fido2.features.{self._name}.enabled = True
|
||||
|
||||
To silence this warning but retain the current behavior, instead set enabled to False:
|
||||
fido2.features.{self._name}.enabled = False
|
||||
|
||||
{self._desc}
|
||||
""",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
|
||||
webauthn_json_mapping = _Feature(
|
||||
"webauthn_json_mapping",
|
||||
"""JSON values for WebAuthn data class Mapping interface.
|
||||
|
||||
This changes the keys and values used by the webauthn data classes when accessed using
|
||||
the Mapping (dict) interface (eg. user_entity["id"] and the from_dict() methods) to be
|
||||
JSON-friendly and align with the current draft of the next WebAuthn Level specification.
|
||||
For the most part, this means that binary values (bytes) are represented as URL-safe
|
||||
base64 encoded strings instead.
|
||||
""",
|
||||
)
|
|
@ -36,6 +36,7 @@ from .utils import (
|
|||
ByteBuffer,
|
||||
_CamelCaseDataObject,
|
||||
)
|
||||
from .features import webauthn_json_mapping
|
||||
from enum import Enum, EnumMeta, unique, IntFlag
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Mapping, Optional, Sequence, Tuple, Union, cast
|
||||
|
@ -406,6 +407,12 @@ class _StringEnum(str, Enum, metaclass=_StringEnumMeta):
|
|||
"""
|
||||
|
||||
|
||||
_b64_metadata = dict(
|
||||
serialize=lambda x: websafe_encode(x) if webauthn_json_mapping.enabled else x,
|
||||
deserialize=lambda x: websafe_decode(x) if webauthn_json_mapping.enabled else x,
|
||||
)
|
||||
|
||||
|
||||
@unique
|
||||
class AttestationConveyancePreference(_StringEnum):
|
||||
NONE = "none"
|
||||
|
@ -461,7 +468,7 @@ class PublicKeyCredentialRpEntity(_CamelCaseDataObject):
|
|||
@dataclass(eq=False, frozen=True)
|
||||
class PublicKeyCredentialUserEntity(_CamelCaseDataObject):
|
||||
name: str
|
||||
id: bytes
|
||||
id: bytes = field(metadata=_b64_metadata)
|
||||
display_name: Optional[str] = None
|
||||
|
||||
|
||||
|
@ -481,7 +488,7 @@ class PublicKeyCredentialParameters(_CamelCaseDataObject):
|
|||
@dataclass(eq=False, frozen=True)
|
||||
class PublicKeyCredentialDescriptor(_CamelCaseDataObject):
|
||||
type: PublicKeyCredentialType
|
||||
id: bytes
|
||||
id: bytes = field(metadata=_b64_metadata)
|
||||
transports: Optional[Sequence[AuthenticatorTransport]] = None
|
||||
|
||||
@classmethod
|
||||
|
@ -521,7 +528,7 @@ class AuthenticatorSelectionCriteria(_CamelCaseDataObject):
|
|||
class PublicKeyCredentialCreationOptions(_CamelCaseDataObject):
|
||||
rp: PublicKeyCredentialRpEntity
|
||||
user: PublicKeyCredentialUserEntity
|
||||
challenge: bytes
|
||||
challenge: bytes = field(metadata=_b64_metadata)
|
||||
pub_key_cred_params: Sequence[PublicKeyCredentialParameters] = field(
|
||||
metadata=dict(deserialize=PublicKeyCredentialParameters._deserialize_list),
|
||||
)
|
||||
|
@ -537,12 +544,12 @@ class PublicKeyCredentialCreationOptions(_CamelCaseDataObject):
|
|||
|
||||
@dataclass(eq=False, frozen=True)
|
||||
class PublicKeyCredentialRequestOptions(_CamelCaseDataObject):
|
||||
challenge: bytes
|
||||
challenge: bytes = field(metadata=_b64_metadata)
|
||||
timeout: Optional[int] = None
|
||||
rp_id: Optional[str] = None
|
||||
allow_credentials: Optional[Sequence[PublicKeyCredentialDescriptor]] = field(
|
||||
default=None,
|
||||
metadata={"deserialize": PublicKeyCredentialDescriptor._deserialize_list},
|
||||
metadata=dict(deserialize=PublicKeyCredentialDescriptor._deserialize_list),
|
||||
)
|
||||
user_verification: Optional[UserVerificationRequirement] = None
|
||||
extensions: Optional[Mapping[str, Any]] = None
|
||||
|
@ -550,16 +557,89 @@ class PublicKeyCredentialRequestOptions(_CamelCaseDataObject):
|
|||
|
||||
@dataclass(eq=False, frozen=True)
|
||||
class AuthenticatorAttestationResponse(_CamelCaseDataObject):
|
||||
client_data: bytes
|
||||
attestation_object: AttestationObject
|
||||
client_data: CollectedClientData = field(
|
||||
metadata=dict(
|
||||
_b64_metadata,
|
||||
name="clientDataJSON",
|
||||
)
|
||||
)
|
||||
attestation_object: AttestationObject = field(metadata=_b64_metadata)
|
||||
extension_results: Optional[Mapping[str, Any]] = None
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == "clientData" and not webauthn_json_mapping.enabled:
|
||||
return self.client_data
|
||||
return super().__getitem__(key)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Optional[Mapping[str, Any]]):
|
||||
if data is not None and not webauthn_json_mapping.enabled:
|
||||
value = dict(data)
|
||||
value["clientDataJSON"] = value.pop("clientData", None)
|
||||
data = value
|
||||
return super().from_dict(data)
|
||||
|
||||
|
||||
@dataclass(eq=False, frozen=True)
|
||||
class AuthenticatorAssertionResponse(_CamelCaseDataObject):
|
||||
client_data: bytes
|
||||
authenticator_data: AuthenticatorData
|
||||
signature: bytes
|
||||
user_handle: bytes
|
||||
credential_id: bytes
|
||||
client_data: CollectedClientData = field(
|
||||
metadata=dict(
|
||||
_b64_metadata,
|
||||
name="clientDataJSON",
|
||||
)
|
||||
)
|
||||
authenticator_data: AuthenticatorData = field(metadata=_b64_metadata)
|
||||
signature: bytes = field(metadata=_b64_metadata)
|
||||
user_handle: Optional[bytes] = field(metadata=_b64_metadata, default=None)
|
||||
credential_id: Optional[bytes] = field(metadata=_b64_metadata, default=None)
|
||||
extension_results: Optional[Mapping[str, Any]] = None
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == "clientData" and not webauthn_json_mapping.enabled:
|
||||
return self.client_data
|
||||
return super().__getitem__(key)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Optional[Mapping[str, Any]]):
|
||||
if data is not None and not webauthn_json_mapping.enabled:
|
||||
value = dict(data)
|
||||
value["clientDataJSON"] = value.pop("clientData", None)
|
||||
data = value
|
||||
print("parse assertion from", data)
|
||||
return super().from_dict(data)
|
||||
|
||||
|
||||
@dataclass(eq=False, frozen=True)
|
||||
class RegistrationResponse(_CamelCaseDataObject):
|
||||
id: bytes = field(metadata=_b64_metadata)
|
||||
response: AuthenticatorAttestationResponse
|
||||
authenticator_attachment: Optional[AuthenticatorAttachment] = None
|
||||
client_extension_results: Optional[Mapping] = None
|
||||
type: Optional[PublicKeyCredentialType] = None
|
||||
|
||||
def __post_init__(self):
|
||||
webauthn_json_mapping.require()
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
@dataclass(eq=False, frozen=True)
|
||||
class AuthenticationResponse(_CamelCaseDataObject):
|
||||
id: bytes = field(metadata=_b64_metadata)
|
||||
response: AuthenticatorAssertionResponse
|
||||
authenticator_attachment: Optional[AuthenticatorAttachment] = None
|
||||
client_extension_results: Optional[Mapping] = None
|
||||
type: Optional[PublicKeyCredentialType] = None
|
||||
|
||||
def __post_init__(self):
|
||||
webauthn_json_mapping.require()
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
@dataclass(eq=False, frozen=True)
|
||||
class CredentialCreationOptions(_CamelCaseDataObject):
|
||||
public_key: PublicKeyCredentialCreationOptions
|
||||
|
||||
|
||||
@dataclass(eq=False, frozen=True)
|
||||
class CredentialRequestOptions(_CamelCaseDataObject):
|
||||
public_key: PublicKeyCredentialRequestOptions
|
||||
|
|
Loading…
Reference in New Issue