glass/browser/session/
webauthn.rs1use super::*;
9
10#[derive(Debug, Clone, serde::Serialize)]
12pub struct WebAuthnOptions {
13 pub protocol: String,
15 pub transport: String,
17 #[serde(default)]
19 pub has_resident_key: bool,
20 #[serde(default)]
22 pub has_user_verification: bool,
23 #[serde(default)]
25 pub is_user_verifying_platform_authenticator: bool,
26}
27
28impl Default for WebAuthnOptions {
29 fn default() -> Self {
30 Self {
31 protocol: "ctap2".into(),
32 transport: "internal".into(),
33 has_resident_key: false,
34 has_user_verification: false,
35 is_user_verifying_platform_authenticator: false,
36 }
37 }
38}
39
40pub struct WebAuthnGuard {
50 cdp: CdpClient,
51 authenticator_id: String,
52 armed: bool,
53}
54
55impl WebAuthnGuard {
56 pub(crate) async fn start(cdp: CdpClient, options: &WebAuthnOptions) -> BrowserResult<Self> {
58 cdp.send("WebAuthn.enable", None).await?;
59 let result = cdp.send("WebAuthn.addVirtualAuthenticator", Some(serde_json::json!({
60 "options": {
61 "protocol": options.protocol,
62 "transport": options.transport,
63 "hasResidentKey": options.has_resident_key,
64 "hasUserVerification": options.has_user_verification,
65 "isUserVerifyingPlatformAuthenticator": options.is_user_verifying_platform_authenticator,
66 }
67 }))).await?;
68 let authenticator_id = result["authenticatorId"]
69 .as_str()
70 .ok_or("WebAuthn.addVirtualAuthenticator returned no authenticatorId")?
71 .to_string();
72 Ok(Self {
73 cdp,
74 authenticator_id,
75 armed: true,
76 })
77 }
78
79 pub fn authenticator_id(&self) -> &str {
81 &self.authenticator_id
82 }
83
84 pub async fn add_credential(
92 &self,
93 credential_id: &str,
94 rp_id: &str,
95 user_handle: &str,
96 private_key_pem: &str,
97 sign_count: u32,
98 ) -> BrowserResult<()> {
99 self.cdp
100 .send(
101 "WebAuthn.addCredential",
102 Some(serde_json::json!({
103 "authenticatorId": self.authenticator_id,
104 "credential": {
105 "credentialId": credential_id,
106 "rpId": rp_id,
107 "privateKey": private_key_pem,
108 "signCount": sign_count,
109 "isResidentCredential": true,
110 "userHandle": user_handle,
111 }
112 })),
113 )
114 .await?;
115 Ok(())
116 }
117
118 pub async fn disable(mut self) -> BrowserResult<()> {
124 self.armed = false;
125 let _ = self
126 .cdp
127 .send(
128 "WebAuthn.removeVirtualAuthenticator",
129 Some(serde_json::json!({"authenticatorId": self.authenticator_id})),
130 )
131 .await;
132 let _ = self.cdp.send("WebAuthn.disable", None).await;
133 Ok(())
134 }
135}
136
137impl Drop for WebAuthnGuard {
138 fn drop(&mut self) {
139 if self.armed {
140 let cdp = self.cdp.clone();
141 let auth_id = self.authenticator_id.clone();
142 tokio::spawn(async move {
143 let _ = cdp
144 .send(
145 "WebAuthn.removeVirtualAuthenticator",
146 Some(serde_json::json!({"authenticatorId": auth_id})),
147 )
148 .await;
149 let _ = cdp.send("WebAuthn.disable", None).await;
150 });
151 }
152 }
153}
154
155impl BrowserSession {
156 pub async fn enable_webauthn(&self, options: &WebAuthnOptions) -> BrowserResult<WebAuthnGuard> {
162 self.cdp
163 .with_current_route(async { WebAuthnGuard::start(self.cdp.clone(), options).await })
164 .await
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn webauthn_options_default_uses_ctap2_and_internal_transport() {
174 let opts = WebAuthnOptions::default();
175 assert_eq!(opts.protocol, "ctap2");
176 assert_eq!(opts.transport, "internal");
177 }
178
179 #[test]
180 fn webauthn_options_default_has_all_booleans_false() {
181 let opts = WebAuthnOptions::default();
182 assert!(!opts.has_resident_key);
183 assert!(!opts.has_user_verification);
184 assert!(!opts.is_user_verifying_platform_authenticator);
185 }
186
187 #[test]
188 fn webauthn_options_serializes_to_json() {
189 let opts = WebAuthnOptions::default();
190 let json = serde_json::to_value(&opts).unwrap();
191 assert_eq!(json["protocol"], "ctap2");
192 assert_eq!(json["transport"], "internal");
193 assert!(!json["has_resident_key"].as_bool().unwrap());
194 assert!(!json["has_user_verification"].as_bool().unwrap());
195 }
196
197 #[test]
198 fn webauthn_options_custom_serializes_all_fields() {
199 let opts = WebAuthnOptions {
200 protocol: "u2f".to_string(),
201 transport: "usb".to_string(),
202 has_resident_key: true,
203 has_user_verification: true,
204 is_user_verifying_platform_authenticator: true,
205 };
206 let json = serde_json::to_value(&opts).unwrap();
207 assert_eq!(json["protocol"], "u2f");
208 assert_eq!(json["transport"], "usb");
209 assert!(json["has_resident_key"].as_bool().unwrap());
210 assert!(json["has_user_verification"].as_bool().unwrap());
211 assert!(
212 json["is_user_verifying_platform_authenticator"]
213 .as_bool()
214 .unwrap()
215 );
216 }
217
218 #[test]
219 fn webauthn_options_with_false_booleans_still_serializes_them() {
220 let opts = WebAuthnOptions {
221 protocol: "ctap2".to_string(),
222 transport: "nfc".to_string(),
223 has_resident_key: false,
224 has_user_verification: false,
225 is_user_verifying_platform_authenticator: false,
226 };
227 let json = serde_json::to_value(&opts).unwrap();
228 assert!(!json["has_resident_key"].as_bool().unwrap());
231 assert!(!json["has_user_verification"].as_bool().unwrap());
232 assert!(
233 !json["is_user_verifying_platform_authenticator"]
234 .as_bool()
235 .unwrap()
236 );
237 }
238}