Extend Fido2Server to directly support data structures produced/consumed by Navigator.credentials interface
Let's say I'm implementing Webauthn in a web application, I'm using python-fido2 on the backend and plain Javascript on the frontend.
On the frontend, the Navigator.credentials.create and Navigator.credentials.get calls consume and produce data structures that need to be transported to/from the backend. They contain ArrayBuffers and so cannot be directly serialized as JSON. One option is to use CBOR+base64 (like the demo code in this repository does), another option is to encode just the binary fields as base64url (what github/webauthn-json does).
Either way, let's say I've managed to transport a response from the browser to the backend, with all the binary fields in tact (ArrayBuffers on the client become bytes instances on the server). On the backend I then must write some glue code to pick fields from client's data, instantiate them as objects, and pass them to one of the Fido2Server's methods. To take register_complete as an example, I must take the clientDataJson field and instantiate fido2.client.ClientData with it, and take the attestationObject field and instantiate fido2.ctap2.AttestationObject with it.
From Webauthn implementer's perspective, it would be great if the Fido2Server class could consume and produce the same data structures as the browser directly, without the glue code. For example,
server.register_complete(state, response_from_client_as_dict)
Would there be interest in having something like that?
Since there's no standard way of serializing the entire response from a WebAuthn operation I'm not sure how we would support a single response_from_client_as_id object without imposing our own structure which is something I would like to avoid.
However, one thing we at least could do is accept bytes for the arguments to register_complete and authenticate_complete and do the instantiation of CollectedClientData, AttestationObject, etc. for you. I think that would make sense for us, and would be an easy improvement. What do you think about that?
Since there's no standard way of serializing the entire response from a WebAuthn operation I'm not sure how we would support a single response_from_client_as_id object without imposing our own structure which is something I would like to avoid.
How about using the exact same structure as browser's Navigator.credentials interfaces use. The objects, strings, ArrayBuffers on the browser side would map to dicts, strings, bytes objects on the python side.
I'm not expecting python-fido2 to handle serialization/deserialization of these structures to wire format (some combination of base64, JSON, CBOR, ...). Although if the serialization format gets standartized, perhaps that would make sense too.
However, one thing we at least could do is accept bytes for the arguments to register_complete and authenticate_complete and do the instantiation of CollectedClientData, AttestationObject, etc. for you.
That would be awesome!
One issue I see with the structure of the response when calling Navigator.credentials.create/get is that it is an object with both properties and methods, so we'd have to map the getter methods to property names to be able to serialize the data, or just ignore the getters, which I suppose would work. If the format does get standardized then I think this is definitely something we should support, but that issue being open is also one of the reasons I hesitate to implement this: I want to avoid implementing "our own thing" if there is going to be a standard format coming soon.
I'm leaning towards taking a "wait and see" approach in hope that a standardized solution will come, but adding the automatic handling of bytes instead of the data classes for now.
I've created a branch where I've implemented support for this in accordance with the current WebAuthn specification draft: #150.
I had to break backwards compatibility for this, so it's added as an opt-in feature that you have to explicitly enable. The server example has also been updated, so you can take a look there to see how it works. It's now using https://github.com/github/webauthn-json on the client side instead of the previous CBOR serialization.
I'm pretty happy with how it turned out, it certainly simplified passing the relevant data between server and client. Let me know if you have any thoughts on this!
Very happy to see this!
I tested my project with this, and could get it to work fairly easily, thanks to the server example. My glue code went down from ~120 lines to ~60 lines.
I had this for recursively converting bytes to base64-encoded strings:
def bytes_to_b64(obj):
"""Return a copy, with any bytes fields converted to base64 strings.
Use this for preparing fido2 data structures for serialization
with the default JSON serializer.
"""
if isinstance(obj, dict) or isinstance(obj, Mapping):
return {k: bytes_to_b64(v) for k, v in obj.items()}
if isinstance(obj, list):
return [bytes_to_b64(v) for v in obj]
if isinstance(obj, bytes):
return websafe_encode(obj)
return obj
And this for mapping JSON structure to python-fido2 classes:
_DECODE_MAP = {
"clientDataJSON": CollectedClientData,
"attestationObject": AttestationObject,
"rawId": bytes,
"authenticatorData": AuthenticatorData,
"signature": bytes,
}
def json_decode_hook(d: dict) -> dict:
"""Base64-decode and instantiate fields listed in _DECODE_MAP.
Use for preparing fido2 data structures from serialized JSON:
>>> json.loads(json_string, object_hook=json_decode_hook)
"""
for key, cls in _DECODE_MAP.items():
if key in d:
as_bytes = websafe_decode(d[key])
d[key] = cls(as_bytes)
return d
– and neither is needed now!
This is now released in python-fido2 1.1!