from dataclasses import dataclass import json from typing import Any, Dict, List, Optional from google.auth import exceptions @dataclass(frozen=True) class PublicKeyCredentialDescriptor: """Descriptor for a security key based credential. https://www.w3.org/TR/webauthn-3/#dictionary-credential-descriptor Args: id: credential id (key handle). transports: <'usb'|'nfc'|'ble'|'internal'> List of supported transports. """ id: str transports: Optional[List[str]] = None def to_dict(self): cred = {"type": "public-key", "id": self.id} if self.transports: cred["transports"] = self.transports return cred @dataclass class AuthenticationExtensionsClientInputs: """Client extensions inputs for WebAuthn extensions. Args: appid: app id that can be asserted with in addition to rpid. https://www.w3.org/TR/webauthn-3/#sctn-appid-extension """ appid: Optional[str] = None def to_dict(self): extensions = {} if self.appid: extensions["appid"] = self.appid return extensions @dataclass class GetRequest: """WebAuthn get request Args: origin: Origin where the WebAuthn get assertion takes place. rpid: Relying Party ID. challenge: raw challenge. timeout_ms: Timeout number in millisecond. allow_credentials: List of allowed credentials. user_verification: <'required'|'preferred'|'discouraged'> User verification requirement. extensions: WebAuthn authentication extensions inputs. """ origin: str rpid: str challenge: str timeout_ms: Optional[int] = None allow_credentials: Optional[List[PublicKeyCredentialDescriptor]] = None user_verification: Optional[str] = None extensions: Optional[AuthenticationExtensionsClientInputs] = None def to_json(self) -> str: req_options: Dict[str, Any] = {"rpid": self.rpid, "challenge": self.challenge} if self.timeout_ms: req_options["timeout"] = self.timeout_ms if self.allow_credentials: req_options["allowCredentials"] = [ c.to_dict() for c in self.allow_credentials ] if self.user_verification: req_options["userVerification"] = self.user_verification if self.extensions: req_options["extensions"] = self.extensions.to_dict() return json.dumps( {"type": "get", "origin": self.origin, "requestData": req_options} ) @dataclass(frozen=True) class AuthenticatorAssertionResponse: """Authenticator response to a WebAuthn get (assertion) request. https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse Args: client_data_json: client data JSON. authenticator_data: authenticator data. signature: signature. user_handle: user handle. """ client_data_json: str authenticator_data: str signature: str user_handle: Optional[str] @dataclass(frozen=True) class GetResponse: """WebAuthn get (assertion) response. Args: id: credential id (key handle). response: The authenticator assertion response. authenticator_attachment: <'cross-platform'|'platform'> The attachment status of the authenticator. client_extension_results: WebAuthn authentication extensions output results in a dictionary. """ id: str response: AuthenticatorAssertionResponse authenticator_attachment: Optional[str] client_extension_results: Optional[Dict] @staticmethod def from_json(json_str: str): """Verify and construct GetResponse from a JSON string.""" try: resp_json = json.loads(json_str) except ValueError: raise exceptions.MalformedError("Invalid Get JSON response") if resp_json.get("type") != "getResponse": raise exceptions.MalformedError( "Invalid Get response type: {}".format(resp_json.get("type")) ) pk_cred = resp_json.get("responseData") if pk_cred is None: if resp_json.get("error"): raise exceptions.ReauthFailError( "WebAuthn.get failure: {}".format(resp_json["error"]) ) else: raise exceptions.MalformedError("Get response is empty") if pk_cred.get("type") != "public-key": raise exceptions.MalformedError( "Invalid credential type: {}".format(pk_cred.get("type")) ) assertion_json = pk_cred["response"] assertion_resp = AuthenticatorAssertionResponse( client_data_json=assertion_json["clientDataJSON"], authenticator_data=assertion_json["authenticatorData"], signature=assertion_json["signature"], user_handle=assertion_json.get("userHandle"), ) return GetResponse( id=pk_cred["id"], response=assertion_resp, authenticator_attachment=pk_cred.get("authenticatorAttachment"), client_extension_results=pk_cred.get("clientExtensionResults"), )