from __future__ import annotations import json from collections.abc import Iterable from secrets import token_bytes from typing import Any import fido2.features from fido2.server import Fido2Server from fido2.webauthn import ( AttestedCredentialData, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, UserVerificationRequirement, ) fido2.features.webauthn_json_mapping.enabled = True class CreateHelper: def __init__(self, rp_id: str, credentials: Iterable[bytes]): rp = PublicKeyCredentialRpEntity(id=rp_id, name="healthchecks") self.server = Fido2Server(rp) self.credentials = [AttestedCredentialData(blob) for blob in credentials] def prepare(self, email: str) -> tuple[dict[str, Any], Any]: # User handle (id) 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 = PublicKeyCredentialUserEntity( id=token_bytes(16), name=email, display_name=email, ) options, state = self.server.register_begin( user, self.credentials, user_verification=UserVerificationRequirement.DISCOURAGED, ) return dict(options), state def verify(self, state: Any, response_json: str) -> bytes | None: doc = json.loads(response_json) auth_data = self.server.register_complete(state, doc) return auth_data.credential_data class GetHelper: def __init__(self, rp_id: str, credentials: Iterable[bytes]): rp = PublicKeyCredentialRpEntity(id=rp_id, name="healthchecks") self.server = Fido2Server(rp) self.credentials = [AttestedCredentialData(blob) for blob in credentials] def prepare(self) -> tuple[dict[str, Any], Any]: options, state = self.server.authenticate_begin(self.credentials) return dict(options), state def verify(self, state: Any, response_json: str) -> bool: try: doc = json.loads(response_json) self.server.authenticate_complete(state, self.credentials, doc) return True except ValueError: return False