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}