1use async_trait::async_trait;
9use bitwarden_threading::ThreadBoundRunner;
10use wasm_bindgen::prelude::*;
11
12use crate::{
13 OnePasswordSsoEnrollment, OnePasswordSsoEnrollmentResult, OnePasswordSsoEnrollmentStatus,
14 OnePasswordSsoLoginResult, OnePasswordSsoUi, OnePasswordTotpResult, OnePasswordTwoFactorUi,
15 OnePasswordVerificationCodeResult,
16};
17
18#[wasm_bindgen(typescript_custom_section)]
19const TS_CUSTOM_TYPES: &'static str = r#"
20/**
21 * Asks the user for a 1Password two-step verification code during a direct import.
22 */
23export interface OnePasswordTwoFactorUi {
24 /**
25 * Provides a TOTP passcode for the given zero-based attempt. A wrong code restarts the
26 * sign-in, so `attempt` grows as the user retries.
27 *
28 * @returns the passcode, or undefined if the user declined to provide one.
29 */
30 provideTotp(attempt: number): Promise<string | undefined>;
31}
32
33/**
34 * Takes the user through a 1Password single sign-on login during a direct import.
35 */
36export interface OnePasswordSsoUi {
37 /**
38 * Opens `ssoLoginUrl`, usually in a browser, and waits for the identity provider to redirect
39 * to `redirectTo`.
40 *
41 * @returns the full URL the identity provider redirected to, or undefined if the user gave up.
42 */
43 performSsoLogin(ssoLoginUrl: string, redirectTo: string): Promise<string | undefined>;
44
45 /**
46 * Shows the device enrollment UI. Called once per import: every import is a new device.
47 */
48 beginSsoEnrollment(): Promise<OnePasswordSsoEnrollment>;
49}
50
51/**
52 * A device enrollment in progress. `endEnrollment` is called exactly once when it ends, unless the
53 * import is dropped halfway.
54 */
55export interface OnePasswordSsoEnrollment {
56 /**
57 * Resolves when the user cancels the enrollment; a rejection counts as a cancellation. Keep it
58 * pending while the enrollment runs, and settle it once `endEnrollment` is called so the
59 * object can be released.
60 */
61 cancelled(): Promise<void>;
62
63 /** Shows the stage the enrollment has reached. */
64 updateStatus(status: OnePasswordSsoEnrollmentStatus, message: string): Promise<void>;
65
66 /**
67 * Asks for the verification code the approving device shows, for the given zero-based
68 * attempt. From the second attempt on, the previous code was wrong and the approving device
69 * shows a new one. The user gets three attempts.
70 *
71 * @returns the code, or undefined if the user declined to provide one.
72 */
73 provideVerificationCode(attempt: number): Promise<string | undefined>;
74
75 /** Closes the enrollment UI. */
76 endEnrollment(result: OnePasswordSsoEnrollmentResult): Promise<void>;
77}
78"#;
79
80#[bitwarden_ffi::wasm_import]
81extern "C" {
82 #[wasm_bindgen(js_name = OnePasswordTwoFactorUi, typescript_type = "OnePasswordTwoFactorUi")]
84 pub type RawJsOnePasswordTwoFactorUi;
85
86 #[wasm_bindgen(catch, method, structural, js_name = provideTotp)]
87 async fn provide_totp(
88 this: &RawJsOnePasswordTwoFactorUi,
89 attempt: u32,
90 ) -> Result<JsValue, JsValue>;
91}
92
93pub(crate) struct JsOnePasswordTwoFactorUi(ThreadBoundRunner<RawJsOnePasswordTwoFactorUi>);
98
99impl JsOnePasswordTwoFactorUi {
100 pub(crate) fn new(ui: RawJsOnePasswordTwoFactorUi) -> Self {
101 Self(ThreadBoundRunner::new(ui))
102 }
103}
104
105#[async_trait::async_trait]
106impl OnePasswordTwoFactorUi for JsOnePasswordTwoFactorUi {
107 async fn provide_totp(&self, attempt: u32) -> OnePasswordTotpResult {
108 let code = self
109 .0
110 .run_in_thread(move |ui| async move {
111 ui.provide_totp(attempt)
112 .await
113 .ok()
114 .and_then(|code| code.as_string())
115 })
116 .await;
117
118 match code {
121 Ok(Some(code)) => OnePasswordTotpResult::Code(code),
122 Ok(None) | Err(_) => OnePasswordTotpResult::Cancel,
123 }
124 }
125}
126
127#[bitwarden_ffi::wasm_import]
128extern "C" {
129 #[wasm_bindgen(js_name = OnePasswordSsoUi, typescript_type = "OnePasswordSsoUi")]
131 pub type RawJsOnePasswordSsoUi;
132
133 #[wasm_bindgen(catch, method, structural, js_name = performSsoLogin)]
134 async fn perform_sso_login(
135 this: &RawJsOnePasswordSsoUi,
136 sso_login_url: &str,
137 redirect_to: &str,
138 ) -> Result<JsValue, JsValue>;
139
140 #[wasm_bindgen(catch, method, structural, js_name = beginSsoEnrollment)]
141 async fn begin_sso_enrollment(
142 this: &RawJsOnePasswordSsoUi,
143 ) -> Result<RawJsOnePasswordSsoEnrollment, JsValue>;
144
145 #[wasm_bindgen(js_name = OnePasswordSsoEnrollment, typescript_type = "OnePasswordSsoEnrollment")]
147 type RawJsOnePasswordSsoEnrollment;
148
149 #[wasm_bindgen(catch, method, structural, js_name = cancelled)]
150 async fn cancelled(this: &RawJsOnePasswordSsoEnrollment) -> Result<JsValue, JsValue>;
151
152 #[wasm_bindgen(catch, method, structural, js_name = updateStatus)]
153 async fn update_status(
154 this: &RawJsOnePasswordSsoEnrollment,
155 status: OnePasswordSsoEnrollmentStatus,
156 message: &str,
157 ) -> Result<JsValue, JsValue>;
158
159 #[wasm_bindgen(catch, method, structural, js_name = provideVerificationCode)]
160 async fn provide_verification_code(
161 this: &RawJsOnePasswordSsoEnrollment,
162 attempt: u32,
163 ) -> Result<JsValue, JsValue>;
164
165 #[wasm_bindgen(catch, method, structural, js_name = endEnrollment)]
166 async fn end_enrollment(
167 this: &RawJsOnePasswordSsoEnrollment,
168 result: OnePasswordSsoEnrollmentResult,
169 ) -> Result<JsValue, JsValue>;
170}
171
172pub(crate) struct JsOnePasswordSsoUi(ThreadBoundRunner<RawJsOnePasswordSsoUi>);
175
176impl JsOnePasswordSsoUi {
177 pub(crate) fn new(ui: RawJsOnePasswordSsoUi) -> Self {
178 Self(ThreadBoundRunner::new(ui))
179 }
180}
181
182#[async_trait]
183impl OnePasswordSsoUi for JsOnePasswordSsoUi {
184 async fn perform_sso_login(
185 &self,
186 sso_login_url: &str,
187 redirect_to: &str,
188 ) -> OnePasswordSsoLoginResult {
189 let sso_login_url = sso_login_url.to_owned();
190 let redirect_to = redirect_to.to_owned();
191 let redirected_to = self
192 .0
193 .run_in_thread(move |ui| async move {
194 ui.perform_sso_login(&sso_login_url, &redirect_to)
195 .await
196 .ok()
197 .and_then(|url| url.as_string())
198 })
199 .await;
200
201 match redirected_to {
202 Ok(Some(url)) => OnePasswordSsoLoginResult::RedirectedTo(url),
203 Ok(None) | Err(_) => OnePasswordSsoLoginResult::Cancel,
204 }
205 }
206
207 async fn begin_sso_enrollment(&self) -> Box<dyn OnePasswordSsoEnrollment> {
208 let enrollment = self
211 .0
212 .run_in_thread(|ui| async move {
213 let enrollment = ui.begin_sso_enrollment().await.ok()?;
214 Some(ThreadBoundRunner::new(enrollment))
215 })
216 .await;
217
218 match enrollment {
219 Ok(Some(runner)) => Box::new(JsOnePasswordSsoEnrollment(runner)),
220 Ok(None) | Err(_) => Box::new(CancelledEnrollment),
221 }
222 }
223}
224
225struct JsOnePasswordSsoEnrollment(ThreadBoundRunner<RawJsOnePasswordSsoEnrollment>);
227
228#[async_trait]
229impl OnePasswordSsoEnrollment for JsOnePasswordSsoEnrollment {
230 async fn cancelled(&self) {
231 let _ = self
233 .0
234 .run_in_thread(|enrollment| async move {
235 let _ = enrollment.cancelled().await;
236 })
237 .await;
238 }
239
240 async fn update_status(&self, status: OnePasswordSsoEnrollmentStatus, message: &str) {
241 let message = message.to_owned();
242 let _ = self
243 .0
244 .run_in_thread(move |enrollment| async move {
245 let _ = enrollment.update_status(status, &message).await;
246 })
247 .await;
248 }
249
250 async fn provide_verification_code(&self, attempt: u32) -> OnePasswordVerificationCodeResult {
251 let code = self
252 .0
253 .run_in_thread(move |enrollment| async move {
254 enrollment
255 .provide_verification_code(attempt)
256 .await
257 .ok()
258 .and_then(|code| code.as_string())
259 })
260 .await;
261
262 match code {
263 Ok(Some(code)) => OnePasswordVerificationCodeResult::Code(code),
264 Ok(None) | Err(_) => OnePasswordVerificationCodeResult::Cancel,
265 }
266 }
267
268 async fn end_enrollment(&self, result: OnePasswordSsoEnrollmentResult) {
269 let _ = self
270 .0
271 .run_in_thread(move |enrollment| async move {
272 let _ = enrollment.end_enrollment(result).await;
273 })
274 .await;
275 }
276}
277
278struct CancelledEnrollment;
280
281#[async_trait]
282impl OnePasswordSsoEnrollment for CancelledEnrollment {
283 async fn cancelled(&self) {}
284
285 async fn update_status(&self, _status: OnePasswordSsoEnrollmentStatus, _message: &str) {}
286
287 async fn provide_verification_code(&self, _attempt: u32) -> OnePasswordVerificationCodeResult {
288 OnePasswordVerificationCodeResult::Cancel
289 }
290
291 async fn end_enrollment(&self, _result: OnePasswordSsoEnrollmentResult) {}
292}