1use crate::{
2 ArcIdentity, AuthClientError,
3 api::{
4 AuthResponseSuccess, IdentityServiceResponseKind, IdentityServiceResponseMessage,
5 InternetIdentityAuthRequest,
6 },
7 idle_manager::{IdleManager, IdleManagerOptions},
8 key::{BaseKeyType, Key, KeyWithRaw},
9 option::{AuthClientLoginOptions, IdleOptions, wasm_js::AuthClientCreateOptions},
10 storage::{
11 KEY_STORAGE_DELEGATION, KEY_STORAGE_KEY, KEY_VECTOR, StoredKey,
12 async_storage::{AuthClientStorage, LocalStorage},
13 },
14 util::{callback::OnSuccess, delegation_chain::DelegationChain},
15};
16use futures::{
17 future::{AbortHandle, Abortable},
18 lock::Mutex as FutureMutex,
19};
20use gloo_events::EventListener;
21use gloo_utils::{format::JsValueSerdeExt, window};
22use ic_agent::{
23 export::Principal,
24 identity::{AnonymousIdentity, DelegatedIdentity, DelegationError, Identity, SignedDelegation},
25};
26use parking_lot::Mutex;
27use serde_wasm_bindgen::from_value;
28use std::{cell::RefCell, fmt, sync::Arc, time::Duration};
29use wasm_bindgen_futures::spawn_local;
30use web_sys::{
31 Location, MessageEvent,
32 wasm_bindgen::{JsCast, JsValue},
33};
34
35const IDENTITY_PROVIDER_DEFAULT: &str = "https://identity.internetcomputer.org";
36const IDENTITY_PROVIDER_ENDPOINT: &str = "#authorize";
37
38const INTERRUPT_CHECK_INTERVAL: Duration = Duration::from_millis(500);
39pub const ERROR_USER_INTERRUPT: &str = "UserInterrupt";
41
42thread_local! {
43 static ACTIVE_LOGIN: RefCell<Option<ActiveLogin>> = const { RefCell::new(None) };
44}
45
46#[derive(Debug)]
49struct ActiveLogin {
50 idp_window: web_sys::Window,
51 _message_handler: EventListener,
52 interruption_check_abort_handle: AbortHandle,
53}
54
55impl Drop for ActiveLogin {
56 fn drop(&mut self) {
57 self.interruption_check_abort_handle.abort();
59 let _ = self.idp_window.close();
61 }
63}
64
65pub(super) struct AuthClientInner {
66 pub identity: Arc<Mutex<ArcIdentity>>,
67 pub key: Key,
68 pub storage: FutureMutex<Box<dyn AuthClientStorage>>,
69 pub chain: Arc<Mutex<Option<DelegationChain>>>,
70 pub idle_manager: Mutex<Option<IdleManager>>,
71 pub idle_options: Option<IdleOptions>,
72}
73
74impl fmt::Debug for AuthClientInner {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.debug_struct("AuthClientInner")
77 .field("key", &self.key)
78 .field("idle_options", &self.idle_options)
79 .finish()
80 }
81}
82
83impl Drop for AuthClientInner {
84 fn drop(&mut self) {
85 ACTIVE_LOGIN.with(|cell| cell.borrow_mut().take());
86 }
87}
88
89#[derive(Clone, Debug)]
93pub struct AuthClient(Arc<AuthClientInner>);
94
95impl AuthClient {
96 const DEFAULT_TIME_TO_LIVE: u64 = 8 * 60 * 60 * 1_000_000_000;
98
99 pub fn builder() -> AuthClientBuilder {
101 AuthClientBuilder::new()
102 }
103
104 pub async fn new() -> Result<Self, AuthClientError> {
106 Self::new_with_options(AuthClientCreateOptions::default()).await
107 }
108
109 pub async fn new_with_options(
111 options: AuthClientCreateOptions,
112 ) -> Result<Self, AuthClientError> {
113 let AuthClientCreateOptions {
114 identity,
115 storage,
116 key_type: _key_type,
117 idle_options,
118 } = options;
119
120 let mut storage = storage.unwrap_or_else(|| Box::new(LocalStorage::new()));
121 let options_identity_is_some = identity.is_some();
122
123 let key = Self::create_or_load_key(identity, storage.as_mut()).await?;
124
125 let (chain, identity) = Self::load_delegation_chain(storage.as_mut(), &key).await;
126
127 let idle_manager =
128 Self::create_idle_manager(&idle_options, &chain, options_identity_is_some);
129
130 Ok(Self(Arc::new(AuthClientInner {
131 identity: Arc::new(Mutex::new(identity)),
132 key,
133 storage: FutureMutex::new(storage),
134 chain: Arc::new(Mutex::new(chain)),
135 idle_manager: Mutex::new(idle_manager),
136 idle_options,
137 })))
138 }
139
140 async fn create_or_load_key(
142 identity: Option<ArcIdentity>,
143 storage: &mut dyn AuthClientStorage,
144 ) -> Result<Key, AuthClientError> {
145 match identity {
146 Some(identity) => Ok(Key::Identity(identity)),
147 None => match storage.get(KEY_STORAGE_KEY).await {
148 Ok(Some(stored_key)) => {
149 let private_key = stored_key.decode()?;
150 Ok(Key::WithRaw(KeyWithRaw::new(private_key)))
151 }
152 Ok(None) => {
153 let mut rng = rand::thread_rng();
154 let private_key = ed25519_dalek::SigningKey::generate(&mut rng).to_bytes();
155 storage
156 .set(KEY_STORAGE_KEY, StoredKey::Raw(private_key))
157 .await?;
158 Ok(Key::WithRaw(KeyWithRaw::new(private_key)))
159 }
160 Err(e) => Err(e.into()),
161 },
162 }
163 }
164
165 fn get_delegation_data(
167 chain: &Option<DelegationChain>,
168 ) -> Option<(Vec<u8>, Vec<SignedDelegation>)> {
169 if let Some(chain_inner) = chain.as_ref() {
170 if chain_inner.is_delegation_valid(None) {
171 let public_key = chain_inner.public_key.clone();
172 let delegations = chain_inner.delegations.clone();
173 Some((public_key, delegations))
174 } else {
175 None
176 }
177 } else {
178 Some((Vec::new(), Vec::new()))
179 }
180 }
181
182 async fn load_delegation_chain(
184 storage: &mut dyn AuthClientStorage,
185 key: &Key,
186 ) -> (Option<DelegationChain>, ArcIdentity) {
187 let mut identity = ArcIdentity::from(key);
188 let mut chain: Option<DelegationChain> = None;
189
190 match storage.get(KEY_STORAGE_DELEGATION).await {
191 Ok(Some(chain_stored)) => {
192 let chain_stored = chain_stored.encode();
193 let chain_result = DelegationChain::from_json(&chain_stored);
194 chain = Some(chain_result);
195
196 let delegation_data = Self::get_delegation_data(&chain);
197
198 match delegation_data {
199 Some((public_key, delegations)) => {
200 if !public_key.is_empty() {
201 identity =
202 ArcIdentity::Delegated(Arc::new(DelegatedIdentity::new_unchecked(
203 public_key,
204 Box::new(key.as_arc_identity()),
205 delegations,
206 )));
207 }
208 }
209 None => {
210 #[cfg(feature = "tracing")]
211 info!("Found invalid delegation chain in storage - clearing credentials");
212 if let Err(_e) = Self::delete_storage(storage).await {
213 #[cfg(feature = "tracing")]
214 error!("Failed to delete storage: {}", _e);
215 }
216 identity = ArcIdentity::Anonymous(Arc::new(AnonymousIdentity));
217 chain = None;
218 }
219 }
220 }
221 Ok(None) => (),
222 Err(_e) => {
223 #[cfg(feature = "tracing")]
224 error!("Failed to load delegation chain from storage: {}", _e);
225 }
226 }
227
228 (chain, identity)
229 }
230
231 fn create_idle_manager(
233 idle_options: &Option<IdleOptions>,
234 chain: &Option<DelegationChain>,
235 identity_is_some: bool,
236 ) -> Option<IdleManager> {
237 if !idle_options
238 .as_ref()
239 .and_then(|o| o.disable_idle)
240 .unwrap_or(false)
241 && (chain.is_some() || identity_is_some)
242 {
243 let idle_manager_options: Option<IdleManagerOptions> = idle_options
244 .as_ref()
245 .map(|o| o.idle_manager_options.clone());
246 Some(IdleManager::new(idle_manager_options))
247 } else {
248 None
249 }
250 }
251
252 pub fn idle_manager(&self) -> Option<IdleManager> {
254 self.0.idle_manager.lock().clone()
255 }
256
257 fn register_default_idle_callback(&self) {
259 if let Some(options) = self.0.idle_options.as_ref() {
260 if options.disable_default_idle_callback.unwrap_or_default() {
261 return;
262 }
263
264 if options.idle_manager_options.on_idle.lock().is_empty() {
265 if let Some(idle_manager) = self.0.idle_manager.lock().as_ref() {
266 let client = self.clone();
267 let callback = move || {
268 let client = client.clone();
269 spawn_local(async move {
270 client.logout(None).await;
271 match window().location().reload() {
272 Ok(_) => (),
273 Err(_e) => {
274 #[cfg(feature = "tracing")]
275 error!("Failed to reload page: {_e:?}");
276 }
277 };
278 });
279 };
280 idle_manager.register_callback(callback);
281 }
282 }
283 }
284 }
285
286 async fn handle_success(
288 &self,
289 message: AuthResponseSuccess,
290 on_success: Option<OnSuccess>,
291 ) -> Result<(), DelegationError> {
292 let _ = ACTIVE_LOGIN.with(|cell| cell.borrow_mut().take());
294
295 let delegations = message.delegations.clone();
296 let user_public_key = message.user_public_key.clone();
297
298 let delegation_chain = DelegationChain {
299 delegations: delegations.clone(),
300 public_key: user_public_key.clone(),
301 };
302
303 self.update_storage_with_delegation(&delegation_chain).await;
304 self.update_identity_with_delegation(
305 &delegation_chain,
306 user_public_key.clone(),
307 delegations.clone(),
308 );
309
310 self.verify_and_fix_authentication(
311 &user_public_key,
312 &delegations,
313 &delegation_chain.to_json(),
314 );
315
316 self.maybe_create_idle_manager();
317
318 if let Some(on_success_cb) = on_success {
319 on_success_cb.0.lock()(message.clone());
320 }
321
322 Ok(())
323 }
324
325 async fn update_storage_with_delegation(&self, delegation_chain: &DelegationChain) {
327 if let Key::WithRaw(key) = &self.0.key {
328 if let Err(_e) = self
329 .0
330 .storage
331 .lock()
332 .await
333 .set(KEY_STORAGE_KEY, StoredKey::Raw(*key.raw_key()))
334 .await
335 {
336 #[cfg(feature = "tracing")]
337 error!("Failed to store key: {}", _e);
338 }
339 }
340
341 let chain_json = delegation_chain.to_json();
342 if let Err(_e) = self
343 .0
344 .storage
345 .lock()
346 .await
347 .set(
348 KEY_STORAGE_DELEGATION,
349 StoredKey::String(chain_json.clone()),
350 )
351 .await
352 {
353 #[cfg(feature = "tracing")]
354 error!("Failed to store delegation: {}", _e);
355 }
356 }
357
358 fn update_identity_with_delegation(
360 &self,
361 delegation_chain: &DelegationChain,
362 user_public_key: Vec<u8>,
363 delegations: Vec<SignedDelegation>,
364 ) {
365 *self.0.chain.lock() = Some(delegation_chain.clone());
366 *self.0.identity.lock() =
367 ArcIdentity::Delegated(Arc::new(DelegatedIdentity::new_unchecked(
368 user_public_key,
369 Box::new(self.0.key.as_arc_identity()),
370 delegations,
371 )));
372 }
373
374 fn verify_and_fix_authentication(
376 &self,
377 user_public_key: &[u8],
378 delegations: &[SignedDelegation],
379 chain_json: &str,
380 ) {
381 if self.is_authenticated() {
382 return;
383 }
384
385 #[cfg(feature = "tracing")]
386 warn!("CRITICAL: is_authenticated() returned false after successful login");
387
388 let _is_not_anonymous = self
389 .identity()
390 .sender()
391 .map(|s| s != Principal::anonymous())
392 .unwrap_or(false);
393 let _has_chain = self.0.chain.lock().is_some();
394 #[cfg(feature = "tracing")]
395 debug!(
396 "is_authenticated(): is_not_anonymous={}, has_chain={}",
397 _is_not_anonymous, _has_chain
398 );
399
400 *self.0.chain.lock() = Some(DelegationChain::from_json(chain_json));
401
402 let is_auth_retry = self.is_authenticated();
403 #[cfg(feature = "tracing")]
404 debug!("After fix attempt: is_authenticated() = {}", is_auth_retry);
405
406 if !is_auth_retry {
407 if let Ok(_principal) = self.identity().sender() {
408 #[cfg(feature = "tracing")]
409 debug!("Current principal: {}", _principal);
410 }
411
412 *self.0.identity.lock() =
413 ArcIdentity::Delegated(Arc::new(DelegatedIdentity::new_unchecked(
414 user_public_key.to_vec(),
415 Box::new(self.0.key.as_arc_identity()),
416 delegations.to_vec(),
417 )));
418
419 let _final_auth_check = self.is_authenticated();
420 #[cfg(feature = "tracing")]
421 debug!("Final check: is_authenticated() = {}", _final_auth_check);
422 }
423 }
424
425 fn maybe_create_idle_manager(&self) {
427 let disable_idle = self
428 .0
429 .idle_options
430 .as_ref()
431 .and_then(|o| o.disable_idle)
432 .unwrap_or(false);
433
434 if self.0.idle_manager.lock().is_none() && !disable_idle {
435 let idle_manager_options = self
436 .0
437 .idle_options
438 .as_ref()
439 .map(|o| o.idle_manager_options.clone());
440 let new_idle_manager = IdleManager::new(idle_manager_options);
441 *self.0.idle_manager.lock() = Some(new_idle_manager);
442
443 if self.0.idle_manager.lock().is_some() {
444 self.register_default_idle_callback();
445 }
446 }
447 }
448
449 pub fn identity(&self) -> Arc<dyn Identity> {
451 self.0.identity.lock().as_arc_identity()
452 }
453
454 pub fn principal(&self) -> Result<Principal, String> {
456 self.identity().sender()
457 }
458
459 pub fn is_authenticated(&self) -> bool {
461 let is_not_anonymous = self
462 .identity()
463 .sender()
464 .map(|s| s != Principal::anonymous())
465 .unwrap_or(false);
466
467 let is_valid_chain = self
468 .0
469 .chain
470 .lock()
471 .as_ref()
472 .is_some_and(|c| c.is_delegation_valid(None));
473
474 is_not_anonymous && is_valid_chain
475 }
476
477 pub fn login(&self) {
479 self.login_with_options(AuthClientLoginOptions::default());
480 }
481
482 pub fn login_with_options(&self, options: AuthClientLoginOptions) {
484 ACTIVE_LOGIN.with(|cell| cell.borrow_mut().take());
485
486 let identity_provider_url = match Self::create_idp_url(&options) {
487 Some(url) => url,
488 None => return,
489 };
490
491 let idp_window = match Self::open_idp_window(&identity_provider_url, &options) {
492 Some(window) => window,
493 None => return,
494 };
495
496 let abort_handle = Self::spawn_interruption_check(&idp_window, &options);
497
498 let _message_handler =
499 self.get_event_handler(idp_window.clone(), identity_provider_url, options);
500
501 let active_login = ActiveLogin {
502 idp_window,
503 _message_handler,
504 interruption_check_abort_handle: abort_handle,
505 };
506
507 ACTIVE_LOGIN.with(|cell| *cell.borrow_mut() = Some(active_login));
508 }
509
510 fn create_idp_url(options: &AuthClientLoginOptions) -> Option<web_sys::Url> {
512 let identity_provider_url = match options.identity_provider {
513 Some(ref url) => url as &str,
514 None => IDENTITY_PROVIDER_DEFAULT,
515 };
516
517 match web_sys::Url::new(identity_provider_url) {
518 Ok(url) => {
519 url.set_hash(IDENTITY_PROVIDER_ENDPOINT);
520 Some(url)
521 }
522 Err(_err) => {
523 #[cfg(feature = "tracing")]
524 {
525 use wasm_bindgen::convert::TryFromJsValue;
526 match String::try_from_js_value(_err) {
527 Ok(msg) => error!("Failed to create URL: {}", msg),
528 Err(_) => error!("Failed to create URL"),
529 };
530 }
531 None
532 }
533 }
534 }
535
536 fn open_idp_window(
538 url: &web_sys::Url,
539 options: &AuthClientLoginOptions,
540 ) -> Option<web_sys::Window> {
541 match window().open_with_url_and_target_and_features(
542 &url.href(),
543 "idpWindow",
544 options.window_opener_features.as_deref().unwrap_or(""),
545 ) {
546 Ok(Some(window_handle)) => Some(window_handle),
547 Ok(None) => {
548 let error_message = "Failed to open IdP window. Check popup blocker.".to_string();
549 if let Some(cb) = &options.on_error {
550 cb.0.lock()(Some(error_message.clone()));
551 }
552 None
553 }
554 Err(e) => {
555 let error_message = format!("Error opening IdP window: {:?}", e);
556 if let Some(cb) = &options.on_error {
557 cb.0.lock()(Some(error_message.clone()));
558 }
559 None
560 }
561 }
562 }
563
564 fn spawn_interruption_check(
566 idp_window: &web_sys::Window,
567 options: &AuthClientLoginOptions,
568 ) -> AbortHandle {
569 let (abort_handle, abort_registration) = AbortHandle::new_pair();
570
571 let interruption_check_task = {
572 let idp_window_clone = idp_window.clone();
573 let on_error_clone = options.on_error.clone();
574
575 async move {
576 gloo_timers::future::sleep(Duration::from_secs(1)).await;
577 loop {
578 if idp_window_clone.closed().unwrap_or(true) {
579 let error_message = ERROR_USER_INTERRUPT.to_string();
580 if let Some(on_error) = on_error_clone {
581 on_error.0.lock()(Some(error_message.clone()));
582 }
583 let _ = ACTIVE_LOGIN.with(|cell| cell.borrow_mut().take());
584 break;
585 }
586 gloo_timers::future::sleep(INTERRUPT_CHECK_INTERVAL).await;
587 }
588 }
589 };
590
591 let abortable_task = Abortable::new(interruption_check_task, abort_registration);
592 spawn_local(async {
593 let _ = abortable_task.await;
594 });
595
596 abort_handle
597 }
598
599 fn get_event_handler(
601 &self,
602 idp_window: web_sys::Window,
603 identity_provider_url: web_sys::Url,
604 options: AuthClientLoginOptions,
605 ) -> EventListener {
606 let client = self.clone();
607
608 let callback = move |event: &web_sys::Event| {
609 let event = match event.dyn_ref::<MessageEvent>() {
610 Some(event) => event,
611 None => return,
612 };
613
614 if event.origin() != identity_provider_url.origin() {
615 return;
616 }
617
618 let message = from_value::<IdentityServiceResponseMessage>(event.data())
619 .map_err(|e| e.to_string());
620
621 let max_time_to_live = options
622 .max_time_to_live
623 .unwrap_or(Self::DEFAULT_TIME_TO_LIVE);
624
625 let handle_error_wrapper = |error: String| {
626 #[cfg(feature = "tracing")]
627 error!("AuthClient login failed in event handler: {}", &error);
628 let _ = ACTIVE_LOGIN.with(|cell| cell.borrow_mut().take());
629 if let Some(on_error_cb) = options.clone().on_error {
630 on_error_cb.0.lock()(Some(error.clone()));
631 }
632 };
633
634 match message.and_then(|m| m.kind()) {
635 Ok(kind) => match kind {
636 IdentityServiceResponseKind::Ready => {
637 client.handle_ready_response(
638 &idp_window,
639 &identity_provider_url,
640 &options,
641 max_time_to_live,
642 &handle_error_wrapper,
643 );
644 }
645 IdentityServiceResponseKind::AuthSuccess(response) => {
646 client.handle_auth_success_response(response, &options);
647 }
648 IdentityServiceResponseKind::AuthFailure(error_message) => {
649 handle_error_wrapper(error_message);
650 }
651 },
652 Err(e) => {
653 handle_error_wrapper(e);
654 }
655 }
656 };
657
658 EventListener::new(&window(), "message", callback)
659 }
660
661 fn handle_ready_response(
663 &self,
664 idp_window: &web_sys::Window,
665 identity_provider_url: &web_sys::Url,
666 options: &AuthClientLoginOptions,
667 max_time_to_live: u64,
668 handle_error_wrapper: &dyn Fn(String),
669 ) {
670 use web_sys::js_sys::{Reflect, Uint8Array};
671
672 let request = InternetIdentityAuthRequest {
673 kind: "authorize-client".to_string(),
674 session_public_key: self.0.key.public_key().expect("Failed to get public key"),
675 max_time_to_live: Some(max_time_to_live),
676 allow_pin_authentication: options.allow_pin_authentication,
677 derivation_origin: options.derivation_origin.clone(),
678 };
679 let request_js_value = match JsValue::from_serde(&request) {
680 Ok(value) => value,
681 Err(err) => {
682 handle_error_wrapper(format!("Failed to serialize request: {}", err));
683 return;
684 }
685 };
686
687 let session_public_key_js = Uint8Array::from(&request.session_public_key[..]).into();
688 if Reflect::set(
689 &request_js_value,
690 &JsValue::from_str("sessionPublicKey"),
691 &session_public_key_js,
692 )
693 .is_err()
694 {
695 handle_error_wrapper("Failed to set sessionPublicKey on request".to_string());
696 return;
697 }
698
699 if let Some(custom_values) = options.custom_values.clone() {
700 Self::set_custom_values(&request_js_value, custom_values, handle_error_wrapper);
701 }
702
703 if idp_window
704 .post_message(&request_js_value, &identity_provider_url.origin())
705 .is_err()
706 {
707 handle_error_wrapper("Failed to post message to IdP window".to_string());
708 }
709 }
710
711 fn set_custom_values(
713 request_js_value: &JsValue,
714 custom_values: serde_json::Map<String, serde_json::Value>,
715 handle_error_wrapper: &dyn Fn(String),
716 ) {
717 for (k, v) in custom_values.into_iter() {
718 match JsValue::from_serde(&v) {
719 Ok(value) => {
720 if web_sys::js_sys::Reflect::set(
721 request_js_value,
722 &JsValue::from_str(&k),
723 &value,
724 )
725 .is_err()
726 {
727 handle_error_wrapper(format!("Failed to set custom value '{}'", k));
728 }
729 }
730 Err(err) => {
731 handle_error_wrapper(format!(
732 "Failed to serialize custom value '{}': {}",
733 k, err
734 ));
735 }
736 }
737 }
738 }
739
740 fn handle_auth_success_response(
742 &self,
743 response: AuthResponseSuccess,
744 options: &AuthClientLoginOptions,
745 ) {
746 let client_clone = self.clone();
747 let on_success = options.on_success.clone();
748 let on_error = options.on_error.clone();
749 spawn_local(async move {
750 if let Err(e) = client_clone.handle_success(response, on_success).await {
751 #[cfg(feature = "tracing")]
752 error!("Error during handle_success: {}", e);
753 if let Some(on_error_cb) = on_error {
754 on_error_cb.0.lock()(Some(format!(
755 "Error processing successful login: {:?}",
756 e
757 )));
758 }
759 }
760 });
761 }
762
763 async fn logout_core(
765 identity: Arc<Mutex<ArcIdentity>>,
766 storage: &mut dyn AuthClientStorage,
767 chain: Arc<Mutex<Option<DelegationChain>>>,
768 return_to: Option<Location>,
769 ) {
770 if let Err(_e) = Self::delete_storage(storage).await {
771 #[cfg(feature = "tracing")]
772 error!("Failed to delete storage: {}", _e);
773 }
774
775 *identity.lock() = ArcIdentity::Anonymous(Arc::new(AnonymousIdentity));
777 chain.lock().take();
778
779 if let Some(location) = return_to {
780 Self::redirect_user(location);
781 }
782 }
783
784 fn redirect_user(location: Location) {
786 let href = match location.href() {
787 Ok(href) => href,
788 Err(_) => {
789 #[cfg(feature = "tracing")]
790 error!("Could not get href from return_to location");
791 return;
792 }
793 };
794
795 let history_redirect_success = window()
796 .history()
797 .and_then(|history| history.push_state_with_url(&JsValue::null(), "", Some(&href)))
798 .is_ok();
799
800 if history_redirect_success {
801 return;
802 }
803
804 if window().location().set_href(&href).is_err() {
806 #[cfg(feature = "tracing")]
807 error!("Failed to set href during logout");
808 }
809 }
810
811 pub async fn logout(&self, return_to: Option<Location>) {
814 if let Some(idle_manager) = self.0.idle_manager.lock().take() {
815 drop(idle_manager);
816 }
817
818 let mut storage_lock = self.0.storage.lock().await;
819 let storage_ref: &mut dyn AuthClientStorage = &mut **storage_lock;
820 Self::logout_core(
821 self.0.identity.clone(),
822 storage_ref,
823 self.0.chain.clone(),
824 return_to,
825 )
826 .await;
827 }
828
829 async fn delete_storage(
831 storage: &mut dyn AuthClientStorage,
832 ) -> Result<(), crate::storage::StorageError> {
833 storage.remove(KEY_STORAGE_KEY).await?;
834 storage.remove(KEY_STORAGE_DELEGATION).await?;
835 storage.remove(KEY_VECTOR).await?;
836 Ok(())
837 }
838}
839
840#[derive(Default)]
842pub struct AuthClientBuilder {
843 identity: Option<ArcIdentity>,
844 storage: Option<Box<dyn AuthClientStorage>>,
845 key_type: Option<BaseKeyType>,
846 idle_options: Option<IdleOptions>,
847}
848
849impl AuthClientBuilder {
850 fn new() -> Self {
852 Self::default()
853 }
854
855 pub fn identity(mut self, identity: ArcIdentity) -> Self {
857 self.identity = Some(identity);
858 self
859 }
860
861 pub fn storage<S>(mut self, storage: S) -> Self
863 where
864 S: AuthClientStorage + 'static,
865 {
866 self.storage = Some(Box::new(storage));
867 self
868 }
869
870 pub fn key_type(mut self, key_type: BaseKeyType) -> Self {
872 self.key_type = Some(key_type);
873 self
874 }
875
876 pub fn idle_options(mut self, idle_options: IdleOptions) -> Self {
878 self.idle_options = Some(idle_options);
879 self
880 }
881
882 fn idle_options_mut(&mut self) -> &mut IdleOptions {
886 self.idle_options.get_or_insert_with(IdleOptions::default)
887 }
888
889 pub fn disable_idle(mut self, disable_idle: bool) -> Self {
891 self.idle_options_mut().disable_idle = Some(disable_idle);
892 self
893 }
894
895 pub fn disable_default_idle_callback(mut self, disable_default_idle_callback: bool) -> Self {
897 self.idle_options_mut().disable_default_idle_callback = Some(disable_default_idle_callback);
898 self
899 }
900
901 pub fn idle_manager_options(mut self, idle_manager_options: IdleManagerOptions) -> Self {
903 self.idle_options_mut().idle_manager_options = idle_manager_options;
904 self
905 }
906
907 pub fn on_idle<F>(mut self, on_idle: F) -> Self
911 where
912 F: FnMut() + Send + 'static,
913 {
914 let options = self.idle_options_mut();
915 options
916 .idle_manager_options
917 .on_idle
918 .lock()
919 .push(Box::new(on_idle));
920 self
921 }
922
923 pub fn idle_timeout(mut self, idle_timeout: u32) -> Self {
925 self.idle_options_mut().idle_manager_options.idle_timeout = Some(idle_timeout);
926 self
927 }
928
929 pub fn scroll_debounce(mut self, scroll_debounce: u32) -> Self {
931 self.idle_options_mut().idle_manager_options.scroll_debounce = Some(scroll_debounce);
932 self
933 }
934
935 pub fn capture_scroll(mut self, capture_scroll: bool) -> Self {
937 self.idle_options_mut().idle_manager_options.capture_scroll = Some(capture_scroll);
938 self
939 }
940
941 pub async fn build(self) -> Result<AuthClient, AuthClientError> {
943 let options = AuthClientCreateOptions {
944 identity: self.identity,
945 storage: self.storage,
946 key_type: self.key_type,
947 idle_options: self.idle_options,
948 };
949
950 AuthClient::new_with_options(options).await
951 }
952}
953
954#[allow(dead_code)]
955#[cfg(test)]
956mod tests {
957 use super::*;
958 use wasm_bindgen_test::*;
959
960 #[test]
961 fn test_idle_options_builder() {
962 let options = IdleOptions::builder()
963 .disable_idle(true)
964 .disable_default_idle_callback(true)
965 .idle_manager_options(
966 IdleManagerOptions::builder()
967 .on_idle(|| {})
968 .idle_timeout(1000)
969 .scroll_debounce(500)
970 .capture_scroll(true)
971 .build(),
972 )
973 .build();
974 assert_eq!(options.disable_idle, Some(true));
975 assert_eq!(options.disable_default_idle_callback, Some(true));
976 assert_eq!(options.idle_manager_options.on_idle.lock().len(), 1);
977 assert_eq!(options.idle_manager_options.idle_timeout, Some(1000));
978 assert_eq!(options.idle_manager_options.scroll_debounce, Some(500));
979 assert_eq!(options.idle_manager_options.capture_scroll, Some(true));
980 }
981
982 #[test]
983 fn test_base_key_type_default() {
984 assert_eq!(BaseKeyType::default(), BaseKeyType::Ed25519);
985 }
986
987 #[test]
988 fn test_auth_client_login_options_builder() {
989 let custom_values = vec![("key".to_string(), "value".into())]
990 .into_iter()
991 .collect();
992
993 let options = AuthClientLoginOptions::builder()
994 .allow_pin_authentication(true)
995 .custom_values(custom_values)
996 .on_error(|_| {})
997 .on_success(|_| {})
998 .build();
999
1000 assert_eq!(options.allow_pin_authentication, Some(true));
1001 assert!(options.on_error.is_some());
1002 assert!(options.on_success.is_some());
1003 assert!(options.custom_values.is_some());
1004 }
1005
1006 #[wasm_bindgen_test]
1007 async fn test_auth_client_builder() {
1008 let mut rng = rand::thread_rng();
1009 let private_key = ed25519_dalek::SigningKey::generate(&mut rng).to_bytes();
1010 let identity = ArcIdentity::Ed25519(Arc::new(
1011 ic_agent::identity::BasicIdentity::from_raw_key(&private_key),
1012 ));
1013
1014 let idle_options = IdleOptions::builder()
1015 .disable_idle(true)
1016 .disable_default_idle_callback(true)
1017 .idle_manager_options(
1018 IdleManagerOptions::builder()
1019 .on_idle(|| {})
1020 .idle_timeout(1000)
1021 .scroll_debounce(500)
1022 .capture_scroll(true)
1023 .build(),
1024 )
1025 .build();
1026
1027 let auth_client = AuthClient::builder()
1028 .identity(identity.clone())
1029 .idle_options(idle_options)
1030 .build()
1031 .await
1032 .unwrap();
1033
1034 assert!(!auth_client.is_authenticated());
1035 assert_eq!(
1036 auth_client.identity().sender().unwrap(),
1037 identity.as_arc_identity().sender().unwrap()
1038 ); }
1040}