Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-ui-web/src/passkey.rs

//! The browser half of the passkey ceremony, through `js_sys::Reflect` — no
//! hand-written JavaScript. WebAuthn Level 3 gives the two conversions that
//! make this small: `PublicKeyCredential.parseCreationOptionsFromJSON` /
//! `parseRequestOptionsFromJSON` turn the server's base64url JSON into the
//! options object, and `credential.toJSON()` turns the answer back into the
//! base64url JSON the server verifies.

use js_sys::{Function, Object, Promise, Reflect};
use serde_json::Value;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;

/// `"<DOMException name>: <message>"` — the NAME is what tells a key that is already enrolled
/// (`InvalidStateError`) from a cancelled touch (`NotAllowedError`); `fmt::Fail::passkey_not_created`
/// reads it. A thrown value with no name keeps only its message.
fn err(e: JsValue) -> String {
    let get = |k: &str| {
        Reflect::get(&e, &k.into())
            .ok()
            .and_then(|m| m.as_string())
            .filter(|s| !s.is_empty())
    };
    let message = get("message").unwrap_or_else(|| format!("{e:?}"));
    match get("name") {
        Some(name) => format!("{name}: {message}"),
        None => message,
    }
}

fn method(target: &JsValue, name: &str) -> Result<Function, String> {
    Reflect::get(target, &name.into())
        .map_err(err)?
        .dyn_into::<Function>()
        .map_err(|_| format!("this browser has no {name}; passkeys need a current browser"))
}

/// `navigator.credentials.<verb>({ publicKey: <parsed options> }).toJSON()`.
async fn ceremony(verb: &str, parser: &str, public_key: &Value) -> Result<Value, String> {
    let window = web_sys::window().ok_or("no window")?;
    let pkc = Reflect::get(&window, &"PublicKeyCredential".into()).map_err(err)?;
    let parsed = method(&pkc, parser)?
        .call1(
            &pkc,
            &js_sys::JSON::parse(&public_key.to_string()).map_err(err)?,
        )
        .map_err(err)?;
    let options = Object::new();
    Reflect::set(&options, &"publicKey".into(), &parsed).map_err(err)?;
    let credentials = Reflect::get(&window.navigator(), &"credentials".into()).map_err(err)?;
    let promise: Promise = method(&credentials, verb)?
        .call1(&credentials, &options)
        .map_err(err)?
        .dyn_into()
        .map_err(|_| "credentials call returned no promise".to_owned())?;
    let credential = JsFuture::from(promise).await.map_err(err)?;
    let json = method(&credential, "toJSON")?
        .call0(&credential)
        .map_err(err)?;
    let text = js_sys::JSON::stringify(&json)
        .map_err(err)?
        .as_string()
        .ok_or("toJSON produced no string")?;
    serde_json::from_str(&text).map_err(|e| e.to_string())
}

/// Register: `options` is the server's `register/start` answer (`ceremony_id` + `publicKey`).
pub async fn create(options: &Value) -> Result<Value, String> {
    ceremony(
        "create",
        "parseCreationOptionsFromJSON",
        &options["publicKey"],
    )
    .await
}

/// Log in: `options` is the server's `login/start` answer.
pub async fn get(options: &Value) -> Result<Value, String> {
    ceremony("get", "parseRequestOptionsFromJSON", &options["publicKey"]).await
}