ic_auth_client/auth_client/
wasm_js.rs

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);
39/// The error message when a user interrupts the authentication process.
40pub const ERROR_USER_INTERRUPT: &str = "UserInterrupt";
41
42thread_local! {
43    static ACTIVE_LOGIN: RefCell<Option<ActiveLogin>> = const { RefCell::new(None) };
44}
45
46/// Holds the resources for an active login process.
47/// When this struct is dropped, it automatically cleans up all associated resources.
48#[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        // Abort the interruption check task.
58        self.interruption_check_abort_handle.abort();
59        // Close the IdP window, ignoring errors if it's already closed.
60        let _ = self.idp_window.close();
61        // The message_handler (EventListener) is automatically dropped, removing the listener.
62    }
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/// The tool for managing authentication and identity.
90///
91/// It maintains the state of the user's identity and provides methods for authentication.
92#[derive(Clone, Debug)]
93pub struct AuthClient(Arc<AuthClientInner>);
94
95impl AuthClient {
96    /// Default time to live for the session in nanoseconds (8 hours).
97    const DEFAULT_TIME_TO_LIVE: u64 = 8 * 60 * 60 * 1_000_000_000;
98
99    /// Create a new [`AuthClientBuilder`] for building an AuthClient.
100    pub fn builder() -> AuthClientBuilder {
101        AuthClientBuilder::new()
102    }
103
104    /// Creates a new [`AuthClient`] with default options.
105    pub async fn new() -> Result<Self, AuthClientError> {
106        Self::new_with_options(AuthClientCreateOptions::default()).await
107    }
108
109    /// Creates a new [`AuthClient`] with the provided options.
110    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    /// Creates a new key if one is not found in storage, otherwise loads the existing key.
141    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    /// Extracts delegation data from a delegation chain if it is valid.
166    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    /// Loads a delegation chain from storage and updates the identity if the chain is valid.
183    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    /// Creates an idle manager if idle detection is not disabled.
232    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    /// Returns the idle manager if it exists.
253    pub fn idle_manager(&self) -> Option<IdleManager> {
254        self.0.idle_manager.lock().clone()
255    }
256
257    /// Registers the default idle callback.
258    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    /// Handles a successful authentication response.
287    async fn handle_success(
288        &self,
289        message: AuthResponseSuccess,
290        on_success: Option<OnSuccess>,
291    ) -> Result<(), DelegationError> {
292        // Take and drop the ActiveLogin struct, which handles all resource cleanup.
293        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    /// Stores the delegation chain and key in storage.
326    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    /// Updates the in-memory identity with the new delegation.
359    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    /// Verifies that the user is authenticated and attempts to fix the state if not.
375    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    /// Creates an idle manager if one does not exist and idle detection is not disabled.
426    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    /// Returns the identity of the user.
450    pub fn identity(&self) -> Arc<dyn Identity> {
451        self.0.identity.lock().as_arc_identity()
452    }
453
454    /// Returns the principal of the user.
455    pub fn principal(&self) -> Result<Principal, String> {
456        self.identity().sender()
457    }
458
459    /// Checks if the user is authenticated.
460    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    /// Logs the user in with default options.
478    pub fn login(&self) {
479        self.login_with_options(AuthClientLoginOptions::default());
480    }
481
482    /// Logs the user in with the provided options.
483    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    /// Creates the identity provider URL.
511    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    /// Opens the identity provider window.
537    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    /// Spawns a task to check for user interruption.
565    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    /// Returns an event handler for the login process.
600    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    /// Handles the `Ready` message from the identity provider.
662    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    /// Sets custom values on the request object.
712    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    /// Handles the `AuthResponseSuccess` message from the identity provider.
741    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    /// Logs out the user and clears the stored identity.
764    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        // Reset this auth client to a non-authenticated state.
776        *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    /// Handles redirecting the user after logout.
785    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        // Fallback to assigning location.href
805        if window().location().set_href(&href).is_err() {
806            #[cfg(feature = "tracing")]
807            error!("Failed to set href during logout");
808        }
809    }
810
811    /// Log the user out.
812    /// If a return URL is provided, the user will be redirected to that URL after logging out.
813    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    /// Deletes the stored keys from the provided storage.
830    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/// Builder for the [`AuthClient`].
841#[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    /// Creates a new [`AuthClientBuilder`].
851    fn new() -> Self {
852        Self::default()
853    }
854
855    /// An optional identity to use as the base. If not provided, an `Ed25519` key pair will be used.
856    pub fn identity(mut self, identity: ArcIdentity) -> Self {
857        self.identity = Some(identity);
858        self
859    }
860
861    /// Optional storage with get, set, and remove methods. Currentry only `LocalStorage` is supported.
862    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    /// The type of key to use for the base key. If not provided, `Ed25519` will be used by default.
871    pub fn key_type(mut self, key_type: BaseKeyType) -> Self {
872        self.key_type = Some(key_type);
873        self
874    }
875
876    /// Options for handling idle timeouts. If not provided, default options will be used.
877    pub fn idle_options(mut self, idle_options: IdleOptions) -> Self {
878        self.idle_options = Some(idle_options);
879        self
880    }
881
882    // --- Methods to configure IdleOptions directly on the builder ---
883
884    /// Helper to get mutable access to idle_options, creating default if None.
885    fn idle_options_mut(&mut self) -> &mut IdleOptions {
886        self.idle_options.get_or_insert_with(IdleOptions::default)
887    }
888
889    /// If set to `true`, disables the idle timeout functionality.
890    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    /// If set to `true`, disables the default idle timeout callback.
896    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    /// Options for the [`IdleManager`] that handles idle timeouts.
902    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    /// Sets a callback to be executed when the system becomes idle.
908    ///
909    /// It is possible to set multiple callbacks.
910    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    /// The duration of inactivity after which the system is considered idle.
924    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    /// A delay for debouncing scroll events.
930    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    /// A flag indicating whether to capture scroll events.
936    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    /// Builds a new [`AuthClient`].
942    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        ); // Check if identity was set
1039    }
1040}