passkey.rs
⎇
Raw
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
7use wasm_bindgen::prelude::*;
8
9#[wasm_bindgen(inline_js = r#"
10export function passkeySupported() {
11 return typeof window.PublicKeyCredential === "function"
12 && typeof PublicKeyCredential.parseRequestOptionsFromJSON === "function"
13 && typeof PublicKeyCredential.parseCreationOptionsFromJSON === "function";
14}
15
16export 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
22export 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"#)]
31extern "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
42pub 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
49pub 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.
57fn 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