Skip to main content

playwright_rs/protocol/
credentials.rs

1//! WebAuthn virtual-authenticator credentials.
2//!
3//! Obtained via [`BrowserContext::credentials`](crate::protocol::BrowserContext::credentials).
4//! Install a virtual authenticator, then register / list / delete passkeys
5//! programmatically to drive `navigator.credentials.create()/get()` ceremonies
6//! in tests without real hardware.
7//!
8//! ```no_run
9//! # use playwright_rs::Playwright;
10//! # async fn ex() -> playwright_rs::Result<()> {
11//! # let pw = Playwright::launch().await?;
12//! # let browser = pw.chromium().launch().await?;
13//! # let context = browser.new_context().await?;
14//! let creds = context.credentials();
15//! creds.install().await?;
16//! let cred = creds.create("example.com", None).await?;
17//! assert_eq!(creds.get(None).await?.len(), 1);
18//! creds.delete(&cred.id).await?;
19//! # Ok(())
20//! # }
21//! ```
22//!
23//! See: <https://playwright.dev/docs/api/class-credentials>
24
25use crate::error::Result;
26use crate::server::channel::Channel;
27use serde_json::json;
28
29/// A virtual WebAuthn credential (passkey) held by the virtual authenticator.
30///
31/// Serializable as well as deserializable so passkeys can be saved with a
32/// storage state and seeded back into a fresh context.
33#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
34#[serde(rename_all = "camelCase")]
35#[non_exhaustive]
36pub struct VirtualCredential {
37    /// Base64url credential ID.
38    pub id: String,
39    /// Relying-party (origin) ID the credential is scoped to.
40    pub rp_id: String,
41    /// Base64url user handle, if the credential has one.
42    #[serde(default)]
43    pub user_handle: String,
44    /// Base64url-encoded PKCS#8 private key.
45    #[serde(default)]
46    pub private_key: String,
47    /// Base64url-encoded public key.
48    #[serde(default)]
49    pub public_key: String,
50    /// The signature counter, which each assertion increments. A relying
51    /// party that checks it rejects one that goes backwards, so a storage
52    /// state carries it. Defaults to 0 when reading a state saved without
53    /// one.
54    #[serde(default)]
55    pub sign_count: u32,
56}
57
58/// Optional fields for [`Credentials::create`]. When omitted, the authenticator
59/// generates them.
60#[derive(Debug, Default, Clone)]
61#[non_exhaustive]
62pub struct CredentialsCreateOptions {
63    /// Explicit base64url credential ID.
64    pub id: Option<String>,
65    /// Base64url user handle to associate.
66    pub user_handle: Option<String>,
67    /// Base64url PKCS#8 private key to import.
68    pub private_key: Option<String>,
69    /// Base64url public key to import.
70    pub public_key: Option<String>,
71    /// Initial signature counter. The authenticator starts at 0 otherwise.
72    pub sign_count: Option<u32>,
73}
74
75impl CredentialsCreateOptions {
76    /// Set an explicit credential ID.
77    pub fn id(mut self, id: impl Into<String>) -> Self {
78        self.id = Some(id.into());
79        self
80    }
81    /// Set the user handle.
82    pub fn user_handle(mut self, user_handle: impl Into<String>) -> Self {
83        self.user_handle = Some(user_handle.into());
84        self
85    }
86    /// Import a specific private key (base64url PKCS#8).
87    pub fn private_key(mut self, private_key: impl Into<String>) -> Self {
88        self.private_key = Some(private_key.into());
89        self
90    }
91    /// Import a specific public key (base64url).
92    pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
93        self.public_key = Some(public_key.into());
94        self
95    }
96    /// Start the signature counter at `sign_count`.
97    pub fn sign_count(mut self, sign_count: u32) -> Self {
98        self.sign_count = Some(sign_count);
99        self
100    }
101}
102
103/// Filters for [`Credentials::get`]. With no filter set, all credentials are
104/// returned.
105#[derive(Debug, Default, Clone)]
106#[non_exhaustive]
107pub struct CredentialsGetOptions {
108    /// Only return credentials scoped to this relying-party ID.
109    pub rp_id: Option<String>,
110    /// Only return the credential with this ID.
111    pub id: Option<String>,
112}
113
114impl CredentialsGetOptions {
115    /// Filter by relying-party ID.
116    pub fn rp_id(mut self, rp_id: impl Into<String>) -> Self {
117        self.rp_id = Some(rp_id.into());
118        self
119    }
120    /// Filter by credential ID.
121    pub fn id(mut self, id: impl Into<String>) -> Self {
122        self.id = Some(id.into());
123        self
124    }
125}
126
127/// Manages the browser context's virtual WebAuthn authenticator.
128///
129/// See: <https://playwright.dev/docs/api/class-credentials>
130#[derive(Clone)]
131pub struct Credentials {
132    channel: Channel,
133}
134
135impl std::fmt::Debug for Credentials {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        f.debug_struct("Credentials").finish_non_exhaustive()
138    }
139}
140
141impl Credentials {
142    pub(crate) fn new(channel: Channel) -> Self {
143        Self { channel }
144    }
145
146    /// Installs a virtual WebAuthn authenticator on the context. Call before
147    /// registering credentials or driving `navigator.credentials` ceremonies.
148    ///
149    /// # Errors
150    ///
151    /// Returns error if:
152    /// - The browser context has been closed
153    /// - Communication with the browser process fails
154    ///
155    /// See: <https://playwright.dev/docs/api/class-credentials#credentials-install>
156    pub async fn install(&self) -> Result<()> {
157        self.channel
158            .send_no_result("credentialsInstall", json!({}))
159            .await
160    }
161
162    /// Registers a virtual credential scoped to `rp_id`, returning the created
163    /// credential (with any authenticator-generated fields filled in).
164    ///
165    /// # Errors
166    ///
167    /// Returns error if:
168    /// - The browser context has been closed
169    /// - Communication with the browser process fails
170    ///
171    /// See: <https://playwright.dev/docs/api/class-credentials#credentials-create>
172    pub async fn create(
173        &self,
174        rp_id: &str,
175        options: impl Into<Option<CredentialsCreateOptions>>,
176    ) -> Result<VirtualCredential> {
177        let options = options.into();
178        let mut params = json!({ "rpId": rp_id });
179        if let Some(o) = options {
180            if let Some(id) = o.id {
181                params["id"] = json!(id);
182            }
183            if let Some(uh) = o.user_handle {
184                params["userHandle"] = json!(uh);
185            }
186            if let Some(pk) = o.private_key {
187                params["privateKey"] = json!(pk);
188            }
189            if let Some(pk) = o.public_key {
190                params["publicKey"] = json!(pk);
191            }
192            if let Some(count) = o.sign_count {
193                params["signCount"] = json!(count);
194            }
195        }
196        #[derive(serde::Deserialize)]
197        struct R {
198            credential: VirtualCredential,
199        }
200        let r: R = self.channel.send("credentialsCreate", params).await?;
201        Ok(r.credential)
202    }
203
204    /// Lists virtual credentials, optionally filtered by relying-party or ID.
205    ///
206    /// # Errors
207    ///
208    /// Returns error if:
209    /// - The browser context has been closed
210    /// - Communication with the browser process fails
211    ///
212    /// See: <https://playwright.dev/docs/api/class-credentials#credentials-get>
213    pub async fn get(
214        &self,
215        options: impl Into<Option<CredentialsGetOptions>>,
216    ) -> Result<Vec<VirtualCredential>> {
217        let options = options.into();
218        let mut params = json!({});
219        if let Some(o) = options {
220            if let Some(rp_id) = o.rp_id {
221                params["rpId"] = json!(rp_id);
222            }
223            if let Some(id) = o.id {
224                params["id"] = json!(id);
225            }
226        }
227        #[derive(serde::Deserialize)]
228        struct R {
229            credentials: Vec<VirtualCredential>,
230        }
231        let r: R = self.channel.send("credentialsGet", params).await?;
232        Ok(r.credentials)
233    }
234
235    /// Deletes the credential with the given ID.
236    ///
237    /// # Errors
238    ///
239    /// Returns error if:
240    /// - The browser context has been closed
241    /// - Communication with the browser process fails
242    ///
243    /// See: <https://playwright.dev/docs/api/class-credentials#credentials-delete>
244    pub async fn delete(&self, id: &str) -> Result<()> {
245        self.channel
246            .send_no_result("credentialsDelete", json!({ "id": id }))
247            .await
248    }
249}