1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
|
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: <url-safe base64-encoded> 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: <url-safe base64-encoded> 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: <url-safe base64-encoded> client data JSON.
authenticator_data: <url-safe base64-encoded> authenticator data.
signature: <url-safe base64-encoded> signature.
user_handle: <url-safe base64-encoded> 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: <url-safe base64-encoded> 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"),
)
|