//! The browser half of WebAuthn. //! //! The browser converts between the base64url wire format and ArrayBuffers itself //! (`parse*OptionsFromJSON` and `toJSON()`), so this is a thin shim. web-sys has WebAuthn //! bindings only behind `--cfg web_sys_unstable_apis`. use wasm_bindgen::prelude::*; #[wasm_bindgen(inline_js = r#" export function passkeySupported() { return typeof window.PublicKeyCredential === "function" && typeof PublicKeyCredential.parseRequestOptionsFromJSON === "function" && typeof PublicKeyCredential.parseCreationOptionsFromJSON === "function"; } export async function passkeyCreate(optionsJson) { const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(JSON.parse(optionsJson).publicKey); const cred = await navigator.credentials.create({ publicKey }); return JSON.stringify(cred.toJSON()); } export async function passkeyGet(optionsJson) { const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(JSON.parse(optionsJson).publicKey); const cred = await navigator.credentials.get({ publicKey }); const json = cred.toJSON(); // webauthn-rs wants the key present, and some browsers leave it out. if (json.response && !("userHandle" in json.response)) json.response.userHandle = null; return JSON.stringify(json); } "#)] extern "C" { #[wasm_bindgen(js_name = passkeySupported)] pub fn supported() -> bool; #[wasm_bindgen(js_name = passkeyCreate, catch)] async fn js_create(options: &str) -> Result; #[wasm_bindgen(js_name = passkeyGet, catch)] async fn js_get(options: &str) -> Result; } pub async fn create(options: &str) -> Result { js_create(options) .await .map_err(error_text) .map(|v| v.as_string().unwrap_or_default()) } pub async fn get(options: &str) -> Result { js_get(options) .await .map_err(error_text) .map(|v| v.as_string().unwrap_or_default()) } /// The browser reports "cancelled" and "no matching passkey" as the same NotAllowedError. fn error_text(e: JsValue) -> String { match js_sys::Reflect::get(&e, &"name".into()) .ok() .and_then(|v| v.as_string()) { Some(name) if name != "NotAllowedError" => { crate::i18n::tr(&format!("The passkey was not used ({name}).")) } _ => crate::i18n::tr("The passkey was not used."), } }