import json from secrets import token_bytes from fido2.client import ClientData from fido2.ctap2 import AttestationObject, AuthenticatorData from fido2.server import Fido2Server from fido2.utils import websafe_encode, websafe_decode from fido2.webauthn import PublicKeyCredentialRpEntity def bytes_to_b64(obj): if isinstance(obj, dict): 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 json_decode_map = { "clientDataJSON": ClientData, "attestationObject": AttestationObject, "rawId": bytes, "authenticatorData": AuthenticatorData, "signature": bytes, } def json_decode_hook(d): for key, cls in json_decode_map.items(): if key in d: as_bytes = websafe_decode(d[key]) d[key] = cls(as_bytes) return d class Server(object): def __init__(self, id, name): self.server = Fido2Server(PublicKeyCredentialRpEntity(id, name)) def register_begin(self, email, credentials): # User handle is used in a username-less authentication, to map a credential # received from browser with an user account in the database. # Since we only use security keys as a second factor, # the user handle is not of much use to us. # # The user handle: # - must not be blank, # - must not be a constant value, # - must not contain personally identifiable information. # So we use random bytes, and don't store them on our end: user = { "id": token_bytes(16), "name": email, "displayName": email, } options, state = self.server.register_begin(user, credentials) return bytes_to_b64(options), state def register_complete(self, state, response_json): doc = json.loads(response_json, object_hook=json_decode_hook) return self.server.register_complete( state, doc["response"]["clientDataJSON"], doc["response"]["attestationObject"], ) def authenticate_begin(self, credentials): options, state = self.server.authenticate_begin(credentials) return bytes_to_b64(options), state def authenticate_complete(self, state, credentials, response_json): doc = json.loads(response_json, object_hook=json_decode_hook) return self.server.authenticate_complete( state, credentials, doc["rawId"], doc["response"]["clientDataJSON"], doc["response"]["authenticatorData"], doc["response"]["signature"], )