passkey.rs
| 1 | //! The browser half of WebAuthn. |
| 2 | //! |
| 3 | //! The browser converts between the base64url wire format and ArrayBuffers itself |
| 4 | //! (`parse*OptionsFromJSON` and `toJSON()`), so this is a thin shim. web-sys has WebAuthn |
| 5 | //! bindings only behind `--cfg web_sys_unstable_apis`. |
| 6 | |
| 7 | use wasm_bindgen::prelude::*; |
| 8 | |
| 9 | #[wasm_bindgen(inline_js = r#" |
| 10 | export function passkeySupported() { |
| 11 | return typeof window.PublicKeyCredential === "function" |
| 12 | && typeof PublicKeyCredential.parseRequestOptionsFromJSON === "function" |
| 13 | && typeof PublicKeyCredential.parseCreationOptionsFromJSON === "function"; |
| 14 | } |
| 15 | |
| 16 | export async function passkeyCreate(optionsJson) { |
| 17 | const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(JSON.parse(optionsJson).publicKey); |
| 18 | const cred = await navigator.credentials.create({ publicKey }); |
| 19 | return JSON.stringify(cred.toJSON()); |
| 20 | } |
| 21 | |
| 22 | export async function passkeyGet(optionsJson) { |
| 23 | const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(JSON.parse(optionsJson).publicKey); |
| 24 | const cred = await navigator.credentials.get({ publicKey }); |
| 25 | const json = cred.toJSON(); |
| 26 | // webauthn-rs wants the key present, and some browsers leave it out. |
| 27 | if (json.response && !("userHandle" in json.response)) json.response.userHandle = null; |
| 28 | return JSON.stringify(json); |
| 29 | } |
| 30 | "#)] |
| 31 | extern "C" { |
| 32 | #[wasm_bindgen(js_name = passkeySupported)] |
| 33 | pub fn supported() -> bool; |
| 34 | |
| 35 | #[wasm_bindgen(js_name = passkeyCreate, catch)] |
| 36 | async fn js_create(options: &str) -> Result<JsValue, JsValue>; |
| 37 | |
| 38 | #[wasm_bindgen(js_name = passkeyGet, catch)] |
| 39 | async fn js_get(options: &str) -> Result<JsValue, JsValue>; |
| 40 | } |
| 41 | |
| 42 | pub async fn create(options: &str) -> Result<String, String> { |
| 43 | js_create(options) |
| 44 | .await |
| 45 | .map_err(error_text) |
| 46 | .map(|v| v.as_string().unwrap_or_default()) |
| 47 | } |
| 48 | |
| 49 | pub async fn get(options: &str) -> Result<String, String> { |
| 50 | js_get(options) |
| 51 | .await |
| 52 | .map_err(error_text) |
| 53 | .map(|v| v.as_string().unwrap_or_default()) |
| 54 | } |
| 55 | |
| 56 | /// The browser reports "cancelled" and "no matching passkey" as the same NotAllowedError. |
| 57 | fn error_text(e: JsValue) -> String { |
| 58 | match js_sys::Reflect::get(&e, &"name".into()) |
| 59 | .ok() |
| 60 | .and_then(|v| v.as_string()) |
| 61 | { |
| 62 | Some(name) if name != "NotAllowedError" => { |
| 63 | crate::i18n::tr(&format!("The passkey was not used ({name}).")) |
| 64 | } |
| 65 | _ => crate::i18n::tr("The passkey was not used."), |
| 66 | } |
| 67 | } |
| 68 |