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}
51
52/// Optional fields for [`Credentials::create`]. When omitted, the authenticator
53/// generates them.
54#[derive(Debug, Default, Clone)]
55#[non_exhaustive]
56pub struct CredentialsCreateOptions {
57    /// Explicit base64url credential ID.
58    pub id: Option<String>,
59    /// Base64url user handle to associate.
60    pub user_handle: Option<String>,
61    /// Base64url PKCS#8 private key to import.
62    pub private_key: Option<String>,
63    /// Base64url public key to import.
64    pub public_key: Option<String>,
65}
66
67impl CredentialsCreateOptions {
68    /// Set an explicit credential ID.
69    pub fn id(mut self, id: impl Into<String>) -> Self {
70        self.id = Some(id.into());
71        self
72    }
73    /// Set the user handle.
74    pub fn user_handle(mut self, user_handle: impl Into<String>) -> Self {
75        self.user_handle = Some(user_handle.into());
76        self
77    }
78    /// Import a specific private key (base64url PKCS#8).
79    pub fn private_key(mut self, private_key: impl Into<String>) -> Self {
80        self.private_key = Some(private_key.into());
81        self
82    }
83    /// Import a specific public key (base64url).
84    pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
85        self.public_key = Some(public_key.into());
86        self
87    }
88}
89
90/// Filters for [`Credentials::get`]. With no filter set, all credentials are
91/// returned.
92#[derive(Debug, Default, Clone)]
93#[non_exhaustive]
94pub struct CredentialsGetOptions {
95    /// Only return credentials scoped to this relying-party ID.
96    pub rp_id: Option<String>,
97    /// Only return the credential with this ID.
98    pub id: Option<String>,
99}
100
101impl CredentialsGetOptions {
102    /// Filter by relying-party ID.
103    pub fn rp_id(mut self, rp_id: impl Into<String>) -> Self {
104        self.rp_id = Some(rp_id.into());
105        self
106    }
107    /// Filter by credential ID.
108    pub fn id(mut self, id: impl Into<String>) -> Self {
109        self.id = Some(id.into());
110        self
111    }
112}
113
114/// Manages the browser context's virtual WebAuthn authenticator.
115///
116/// See: <https://playwright.dev/docs/api/class-credentials>
117#[derive(Clone)]
118pub struct Credentials {
119    channel: Channel,
120}
121
122impl std::fmt::Debug for Credentials {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        f.debug_struct("Credentials").finish_non_exhaustive()
125    }
126}
127
128impl Credentials {
129    pub(crate) fn new(channel: Channel) -> Self {
130        Self { channel }
131    }
132
133    /// Installs a virtual WebAuthn authenticator on the context. Call before
134    /// registering credentials or driving `navigator.credentials` ceremonies.
135    ///
136    /// # Errors
137    ///
138    /// Returns error if:
139    /// - The browser context has been closed
140    /// - Communication with the browser process fails
141    ///
142    /// See: <https://playwright.dev/docs/api/class-credentials#credentials-install>
143    pub async fn install(&self) -> Result<()> {
144        self.channel
145            .send_no_result("credentialsInstall", json!({}))
146            .await
147    }
148
149    /// Registers a virtual credential scoped to `rp_id`, returning the created
150    /// credential (with any authenticator-generated fields filled in).
151    ///
152    /// # Errors
153    ///
154    /// Returns error if:
155    /// - The browser context has been closed
156    /// - Communication with the browser process fails
157    ///
158    /// See: <https://playwright.dev/docs/api/class-credentials#credentials-create>
159    pub async fn create(
160        &self,
161        rp_id: &str,
162        options: impl Into<Option<CredentialsCreateOptions>>,
163    ) -> Result<VirtualCredential> {
164        let options = options.into();
165        let mut params = json!({ "rpId": rp_id });
166        if let Some(o) = options {
167            if let Some(id) = o.id {
168                params["id"] = json!(id);
169            }
170            if let Some(uh) = o.user_handle {
171                params["userHandle"] = json!(uh);
172            }
173            if let Some(pk) = o.private_key {
174                params["privateKey"] = json!(pk);
175            }
176            if let Some(pk) = o.public_key {
177                params["publicKey"] = json!(pk);
178            }
179        }
180        #[derive(serde::Deserialize)]
181        struct R {
182            credential: VirtualCredential,
183        }
184        let r: R = self.channel.send("credentialsCreate", params).await?;
185        Ok(r.credential)
186    }
187
188    /// Lists virtual credentials, optionally filtered by relying-party or ID.
189    ///
190    /// # Errors
191    ///
192    /// Returns error if:
193    /// - The browser context has been closed
194    /// - Communication with the browser process fails
195    ///
196    /// See: <https://playwright.dev/docs/api/class-credentials#credentials-get>
197    pub async fn get(
198        &self,
199        options: impl Into<Option<CredentialsGetOptions>>,
200    ) -> Result<Vec<VirtualCredential>> {
201        let options = options.into();
202        let mut params = json!({});
203        if let Some(o) = options {
204            if let Some(rp_id) = o.rp_id {
205                params["rpId"] = json!(rp_id);
206            }
207            if let Some(id) = o.id {
208                params["id"] = json!(id);
209            }
210        }
211        #[derive(serde::Deserialize)]
212        struct R {
213            credentials: Vec<VirtualCredential>,
214        }
215        let r: R = self.channel.send("credentialsGet", params).await?;
216        Ok(r.credentials)
217    }
218
219    /// Deletes the credential with the given ID.
220    ///
221    /// # Errors
222    ///
223    /// Returns error if:
224    /// - The browser context has been closed
225    /// - Communication with the browser process fails
226    ///
227    /// See: <https://playwright.dev/docs/api/class-credentials#credentials-delete>
228    pub async fn delete(&self, id: &str) -> Result<()> {
229        self.channel
230            .send_no_result("credentialsDelete", json!({ "id": id }))
231            .await
232    }
233}