ic_auth_client/
lib.rs

1//! Simple interface to get your web application authenticated with the Internet Identity Service for Rust.
2//!
3//! This crate is intended for use in front-end WebAssembly environments in conjunction with [ic-agent](https://docs.rs/ic-agent).
4
5use crate::{
6    idle_manager::{IdleManager, IdleManagerOptions},
7    storage::{
8        AuthClientStorage, AuthClientStorageType, KEY_STORAGE_DELEGATION, KEY_STORAGE_KEY, KEY_VECTOR,
9    },
10    util::{delegation_chain::DelegationChain, sleep::sleep},
11};
12use ed25519_consensus::SigningKey;
13use gloo_events::EventListener;
14use gloo_utils::{format::JsValueSerdeExt, window};
15use ic_agent::{
16    export::Principal,
17    identity::{AnonymousIdentity, BasicIdentity, DelegatedIdentity, SignedDelegation, DelegationError},
18    Identity,
19};
20use rand::thread_rng;
21use serde::{Deserialize, Serialize};
22use serde_wasm_bindgen::from_value;
23use std::{
24    collections::HashMap,
25    fmt,
26    sync::{Arc, Mutex},
27    cell::RefCell,
28    sync::atomic::{AtomicUsize, Ordering, AtomicBool},
29};
30use storage::StoredKey;
31#[cfg(target_family = "wasm")]
32use wasm_bindgen_futures::spawn_local;
33#[cfg(not(target_family = "wasm"))]
34use tokio::task::spawn_local;
35use web_sys::{Location, MessageEvent, wasm_bindgen::{JsCast, JsValue}};
36
37pub mod idle_manager;
38pub mod storage;
39mod util;
40
41pub use util::delegation_chain;
42
43thread_local! {
44    static EVENT_HANDLERS: RefCell<HashMap<usize, EventListener>> = RefCell::new(HashMap::new());
45    static IDP_WINDOWS: RefCell<HashMap<usize, web_sys::Window>> = RefCell::new(HashMap::new());
46}
47
48type OnSuccess = Arc<Mutex<Box<dyn FnMut(AuthResponseSuccess) + Send>>>;
49type OnError = Arc<Mutex<Box<dyn FnMut(Option<String>) + Send>>>;
50
51const IDENTITY_PROVIDER_DEFAULT: &str = "https://identity.ic0.app";
52const IDENTITY_PROVIDER_ENDPOINT: &str = "#authorize";
53
54const ED25519_KEY_LABEL: &str = "Ed25519";
55
56const INTERRUPT_CHECK_INTERVAL: u64 = 500;
57/// The error message when a user interrupts the authentication process.
58pub const ERROR_USER_INTERRUPT: &str = "UserInterrupt";
59
60/// Represents an Internet Identity authentication request.
61///
62/// This struct is used to send an authentication request to the Internet Identity Service.
63/// It includes all the necessary parameters that the Internet Identity Service needs to authenticate a user.
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
65#[serde(rename_all = "camelCase")]
66struct InternetIdentityAuthRequest {
67    /// The kind of request. This is typically set to "authorize-client".
68    pub kind: String,
69    /// The public key of the session.
70    pub session_public_key: Vec<u8>,
71    /// The maximum time to live for the session, in nanoseconds. If not provided, a default value is used.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub max_time_to_live: Option<u64>,
74    /// If present, indicates whether or not the Identity Provider should allow the user to authenticate and/or register using a temporary key/PIN identity.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub allow_pin_authentication: Option<bool>,
77    /// Origin for Identity Provider to use while generating the delegated identity. For II, the derivation origin must authorize this origin by setting a record at `<derivation-origin>/.well-known/ii-alternative-origins`.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub derivation_origin: Option<String>,
80}
81
82
83/// Represents a successful authentication response.
84///
85/// This struct is used to store the details of a successful authentication response from the Internet Identity Service.
86/// It includes the delegations, the user's public key, and the authentication method used.
87#[derive(Debug, Clone)]
88pub struct AuthResponseSuccess {
89    /// The delegations provided by the user during the authentication process.
90    pub delegations: Vec<SignedDelegation>,
91    /// The public key of the user.
92    pub user_public_key: Vec<u8>,
93    /// The authentication method used by the user.
94    pub authn_method: String,
95}
96
97/// Represents a response message from the Identity Service.
98///
99/// This struct is used to store the details of a response message from the Identity Service.
100/// It includes the kind of the response, the delegations, the user's public key, the authentication method used,
101/// and the error message in case of failure.
102#[derive(Debug, Clone, Deserialize)]
103#[serde(rename_all = "camelCase")]
104struct IdentityServiceResponseMessage {
105    /// The kind of the response. This is typically set to "authorize-ready", "authorize-client-success", or "authorize-client-failure".
106    kind: String,
107
108    /// The delegations provided by the user during the authentication process. This is present in case of a successful authentication.
109    delegations: Option<Vec<SignedDelegation>>,
110
111    /// The public key of the user. This is present in case of a successful authentication.
112    user_public_key: Option<Vec<u8>>,
113
114    /// The authentication method used by the user. This is present in case of a successful authentication.
115    authn_method: Option<String>,
116
117    /// The error message in case of a failed authentication.
118    text: Option<String>,
119}
120
121impl IdentityServiceResponseMessage {
122    /// Returns the kind of the Identity Service response.
123    pub fn kind(&self) -> Result<IdentityServiceResponseKind, String> {
124        match self.kind.as_str() {
125            "authorize-ready" => Ok(IdentityServiceResponseKind::Ready),
126            "authorize-client-success" => Ok(IdentityServiceResponseKind::AuthSuccess(
127                AuthResponseSuccess {
128                    delegations: self.delegations.clone().unwrap_or_default(),
129                    user_public_key: self.user_public_key.clone().unwrap_or_default(),
130                    authn_method: self.authn_method.clone().unwrap_or_default(),
131                }
132            )),
133            "authorize-client-failure" => Ok(IdentityServiceResponseKind::AuthFailure(
134                self.text.clone().unwrap_or_default()
135            )),
136            other => Err(format!("Unknown response kind: {}", other)),
137        }
138    }
139}
140
141/// Enum representing the kind of response from the Identity Service.
142#[derive(Debug, Clone)]
143enum IdentityServiceResponseKind {
144    /// Represents a ready state.
145    Ready,
146    /// Represents a successful authentication response.
147    AuthSuccess(AuthResponseSuccess),
148    /// Represents a failed authentication response with an error message.
149    AuthFailure(String),
150}
151
152/// The tool for managing authentication and identity.
153/// It maintains the state of the user's identity and provides methods for authentication.
154#[derive(Clone, Debug)]
155pub struct AuthClient {
156    /// The user's identity. This can be an anonymous identity, an Ed25519 identity, or a delegated identity.
157    identity: ArcIdentity,
158    /// The key associated with the user's identity.
159    key: ArcIdentity,
160    /// The storage used to persist the user's identity and key.
161    storage: AuthClientStorageType,
162    /// The delegation chain associated with the user's identity.
163    chain: Arc<Mutex<Option<DelegationChain>>>,
164    /// The idle manager that handles idle timeouts.
165    pub idle_manager: Option<IdleManager>,
166    /// The options for handling idle timeouts.
167    idle_options: Option<IdleOptions>,
168    /// A unique identifier for this instance and its clones, used to associate it with thread-local resources.
169    /// Wrapped in Arc to manage cleanup only when the last clone is dropped.
170    id: Arc<usize>,
171    /// Flag to indicate if the current login flow has completed (success, failure, or interrupt).
172    login_complete: Arc<AtomicBool>,
173}
174
175impl Drop for AuthClient {
176    fn drop(&mut self) {
177        // Clean up the thread-local storage only when the last Arc pointing to the id is dropped.
178        if Arc::strong_count(&self.id) == 1 {
179            let id_val = *self.id; // Get the actual ID value
180
181            // Use try_borrow_mut to avoid panic if already borrowed, e.g., during event handling
182            EVENT_HANDLERS.with(|cell| {
183                match cell.try_borrow_mut() {
184                    Ok(mut map) => {
185                        map.remove(&id_val);
186                    }
187                    Err(_) => {
188                        eprintln!("AuthClient::drop: Could not remove event handler for id {} (already borrowed)", id_val);
189                    }
190                }
191            });
192
193            IDP_WINDOWS.with(|cell| {
194                match cell.try_borrow_mut() {
195                    Ok(mut map) => {
196                        // Close the window if it exists before removing it
197                        if let Some(window) = map.remove(&id_val) {
198                             // Ignore error if window is already closed
199                            let _ = window.close();
200                        }
201                    }
202                    Err(_) => {
203                        eprintln!("AuthClient::drop: Could not remove IDP window for id {} (already borrowed)", id_val);
204                    }
205                }
206            });
207        }
208    }
209}
210
211impl AuthClient {
212    /// Sets the event handler for this instance in thread-local storage.
213    fn set_event_handler(&self, handler: EventListener) {
214        EVENT_HANDLERS.with(|cell| {
215            let mut map = cell.borrow_mut();
216            map.insert(*self.id, handler);
217        });
218    }
219
220    /// Takes the event handler for this instance, removing it from thread-local storage.
221    fn take_event_handler(&self) -> Option<EventListener> {
222        EVENT_HANDLERS.with(|cell| {
223            let mut map = cell.borrow_mut();
224            map.remove(&self.id)
225        })
226    }
227
228    /// Sets the IdP window for this instance in thread-local storage.
229    fn set_idp_window(&self, window: web_sys::Window) {
230        IDP_WINDOWS.with(|cell| {
231            let mut map = cell.borrow_mut();
232            map.insert(*self.id, window);
233        });
234    }
235
236    /// Gets the IdP window for this instance from thread-local storage.
237    fn get_idp_window(&self) -> Option<web_sys::Window> {
238        IDP_WINDOWS.with(|cell| {
239            let map = cell.borrow();
240            map.get(&self.id).cloned()
241        })
242    }
243
244    /// Takes the IdP window for this instance, removing it from thread-local storage.
245    fn take_idp_window(&self) -> Option<web_sys::Window> {
246        IDP_WINDOWS.with(|cell| {
247            let mut map = cell.borrow_mut();
248            map.remove(&self.id)
249        })
250    }
251
252    /// Default time to live for the session in nanoseconds (8 hours).
253    const DEFAULT_TIME_TO_LIVE: u64 = 8 * 60 * 60 * 1_000_000_000;
254
255    /// Create a new [`AuthClientBuilder`] for building an AuthClient.
256    pub fn builder() -> AuthClientBuilder {
257        AuthClientBuilder::new()
258    }
259
260    /// Creates a new [`AuthClient`] with default options.
261    pub async fn new() -> Result<Self, DelegationError> {
262        Self::new_with_options(AuthClientCreateOptions::default()).await
263    }
264
265    /// Creates a new [`AuthClient`] with the provided options.
266    pub async fn new_with_options(options: AuthClientCreateOptions) -> Result<Self, DelegationError> {
267        let mut storage = options.storage.unwrap_or_default();
268
269        let mut key: Option<ArcIdentity> = None;
270
271        if let Some(identity) = options.identity.clone() {
272            key = Some(identity);
273        } else {
274            let maybe_local_storage = storage.get(KEY_STORAGE_KEY).await;
275
276            if let Some(maybe_local_storage) = maybe_local_storage {
277                let private_key = maybe_local_storage.decode();
278
279                match private_key {
280                    Ok(private_key) => {
281                        key = Some(
282                            ArcIdentity::Ed25519(Arc::new(
283                                BasicIdentity::from_signing_key(private_key),
284                            ))
285                        );
286                    }
287                    Err(e) => {
288                        return Err(
289                            DelegationError::IdentityError(format!(
290                                "Failed to decode private key: {}",
291                                e
292                            ))
293                        );
294                    }
295                }
296            }
297        }
298
299        let mut identity = ArcIdentity::Anonymous(Arc::new(AnonymousIdentity));
300        let mut chain: Arc<Mutex<Option<DelegationChain>>> = Arc::new(Mutex::new(None));
301
302        let options_identity_is_some = options.identity.is_some();
303
304        // Ensure we have a valid key - this is critical for authentication
305        if key.is_none() {
306            // Generate a new signing key if none was found in storage
307            let private_key = SigningKey::new(thread_rng());
308
309            // Save this key to storage immediately
310            storage
311                .set(KEY_STORAGE_KEY, StoredKey::encode(&private_key))
312                .await;
313
314            key = Some(ArcIdentity::Ed25519(Arc::new(
315                BasicIdentity::from_signing_key(private_key),
316            )));
317        }
318
319        // Now we definitely have a key, we can load delegation if it exists
320        let chain_storage = storage.get(KEY_STORAGE_DELEGATION).await;
321
322        if let Some(op_identity) = options.identity {
323            identity = op_identity;
324        } else if let Some(chain_storage) = chain_storage {
325            match chain_storage {
326                StoredKey::String(chain_storage) => {
327                    // Try to load the delegation chain
328                    let chain_result = DelegationChain::from_json(&chain_storage);
329                    chain = Arc::new(Mutex::new(Some(chain_result)));
330
331                    // First, extract the needed data from the lock without holding it across await
332                    let delegation_data = {
333                        if let Ok(guard) = chain.lock() {
334                            if let Some(chain_inner) = guard.as_ref() {
335                                if chain_inner.is_delegation_valid(None) {
336                                    // Extract the data we need while we have the lock
337                                    let public_key = chain_inner.public_key.clone();
338                                    let delegations = chain_inner.delegations.clone();
339                                    Some((public_key, delegations))
340                                } else {
341                                    // Signal we need to delete storage if delegation is invalid
342                                    None
343                                }
344                            } else {
345                                // No chain data
346                                Some((Vec::new(), Vec::new()))
347                            }
348                        } else {
349                            // Couldn't get lock
350                            Some((Vec::new(), Vec::new()))
351                        }
352                    };
353
354                    // Now use the extracted data without holding the lock
355                    match delegation_data {
356                        Some((public_key, delegations)) => {
357                            if !public_key.is_empty() {
358                                // Create the delegated identity using our key
359                                identity = ArcIdentity::Delegated(Arc::new(DelegatedIdentity::new_unchecked(
360                                    public_key,
361                                    Box::new(key.clone().unwrap().as_arc_identity()),
362                                    delegations,
363                                )));
364                            }
365                        }
366                        None => {
367                            // Need to delete storage - delegation chain is invalid
368                            eprintln!("Found invalid delegation chain in storage - clearing credentials");
369                            Self::delete_storage(&mut storage).await;
370
371                            // Reset to anonymous identity
372                            identity = ArcIdentity::Anonymous(Arc::new(AnonymousIdentity));
373                            chain = Arc::new(Mutex::new(None));
374                        }
375                    }
376                }
377            }
378        }
379
380        let mut idle_manager: Option<IdleManager> = None;
381        if !options
382            .idle_options
383            .as_ref()
384            .and_then(|o| o.disable_idle)
385            .unwrap_or(false)
386            && (chain.lock().unwrap().is_some() || options_identity_is_some)
387        {
388            let idle_manager_options: Option<IdleManagerOptions> = options
389                .idle_options
390                .as_ref()
391                .map(|o| o.idle_manager_options.clone());
392            idle_manager = Some(IdleManager::new(idle_manager_options));
393        }
394
395        if key.is_none() {
396            let private_key = SigningKey::new(thread_rng());
397
398            storage
399                .set(KEY_STORAGE_KEY, StoredKey::encode(&private_key))
400                .await;
401
402            key = Some(ArcIdentity::Ed25519(Arc::new(
403                BasicIdentity::from_signing_key(private_key),
404            )));
405        }
406
407        // Generate a unique ID for this instance
408        let id = {
409            static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
410            // Use Relaxed ordering as we only need atomicity, not synchronization
411            Arc::new(NEXT_ID.fetch_add(1, Ordering::Relaxed))
412        };
413
414        Ok(
415            Self {
416                identity,
417                key: key.unwrap(),
418                storage,
419                chain,
420                idle_manager,
421                idle_options: options.idle_options,
422                id,
423                login_complete: Arc::new(AtomicBool::new(false)),
424            }
425        )
426    }
427
428    /// Registers the default idle callback.
429    fn register_default_idle_callback(
430        identity: ArcIdentity,
431        storage: AuthClientStorageType,
432        chain: Arc<Mutex<Option<DelegationChain>>>,
433        idle_manager: Option<IdleManager>,
434        idle_options: Option<IdleOptions>,
435    ) {
436        if let Some(options) = idle_options.as_ref() {
437            if options.disable_default_idle_callback.unwrap_or_default() {
438                return;
439            }
440
441            if options
442                .idle_manager_options
443                .on_idle
444                .as_ref()
445                .lock()
446                .unwrap()
447                .is_empty()
448            {
449                if let Some(idle_manager) = idle_manager.as_ref() {
450                    let identity = identity.clone();
451                    let storage = storage.clone();
452                    let chain = chain.clone();
453                    let callback = move || {
454                        let mut identity = identity.clone();
455                        let storage = storage.clone();
456                        let chain = chain.clone();
457                        spawn_local(async move {
458                            Self::logout_core(&mut identity, storage, chain, None).await;
459                            window().location().reload().unwrap();
460                        });
461                    };
462                    idle_manager.register_callback(callback);
463                }
464            }
465        }
466    }
467
468    /// Handles a successful authentication response.
469    async fn handle_success(
470        &mut self,
471        message: AuthResponseSuccess,
472        on_success: Option<OnSuccess>,
473    ) -> Result<(), DelegationError>
474    {
475        // Signal that login has completed normally *before* closing the window or calling callbacks.
476        // This prevents the check_interruption task from incorrectly flagging a user interrupt.
477        self.login_complete.store(true, Ordering::SeqCst);
478
479        // Clean up window and event handler *before* potentially long-running async operations or callbacks
480        if let Some(w) = self.take_idp_window() {
481             // Ignore error if window is already closed
482            let _ = w.close();
483        };
484        self.take_event_handler(); // Remove event handler associated with this login attempt
485
486        let delegations = message.delegations.clone();
487        let user_public_key = message.user_public_key.clone();
488
489        // Create the delegation chain
490        let delegation_chain = DelegationChain {
491            delegations: delegations.clone(),
492            public_key: user_public_key.clone(),
493        };
494
495        // Serialize the chain to JSON
496        let chain_json = delegation_chain.to_json();
497
498        // First, save to storage immediately to ensure consistency between refreshes
499        // This is critical for authentication persistence
500        self.storage
501            .set(KEY_STORAGE_DELEGATION, chain_json.clone())
502            .await;
503
504        // Now update the in-memory state
505        self.chain = Arc::new(Mutex::new(Some(delegation_chain.clone())));
506
507        self.identity = ArcIdentity::Delegated(Arc::new(
508            DelegatedIdentity::new_unchecked(
509                user_public_key.clone(),
510                Box::new(self.key.as_arc_identity()),
511                delegations.clone(),
512            )
513        ));
514
515        // Verify authentication state is correct
516        let is_auth = self.is_authenticated();
517        if !is_auth {
518            // This is a severe issue - our in-memory state says we're authenticated,
519            // but is_authenticated() disagrees
520            eprintln!("CRITICAL: is_authenticated() returned false after successful login");
521
522            // Debug the state to understand why is_authenticated() is returning false
523            let is_not_anonymous = self
524                .identity()
525                .sender()
526                .map(|s| s != Principal::anonymous())
527                .unwrap_or(false);
528
529            let has_chain = if let Ok(guard) = self.chain.lock() {
530                guard.is_some()
531            } else {
532                false
533            };
534
535            eprintln!("Debug is_authenticated(): is_not_anonymous={}, has_chain={}", is_not_anonymous, has_chain);
536
537            // Try a more direct approach - recreate the delegation chain from JSON
538            // This ensures our in-memory and storage states are completely in sync
539            if let Ok(mut guard) = self.chain.lock() {
540                *guard = Some(DelegationChain::from_json(&chain_json));
541            }
542
543            // Check again after our fix attempt
544            let is_auth_retry = self.is_authenticated();
545            eprintln!("After fix attempt: is_authenticated() = {}", is_auth_retry);
546
547            // If still failing, provide detailed debug information but DO NOT reload
548            // Let's try to make it work without a reload
549            if !is_auth_retry {
550                if let Ok(principal) = self.identity().sender() {
551                    eprintln!("Current principal: {}", principal);
552                }
553
554                // Attempt one final fix: completely reconstruct the delegated identity
555                self.identity = ArcIdentity::Delegated(Arc::new(
556                    DelegatedIdentity::new_unchecked(
557                        user_public_key.clone(),
558                        Box::new(self.key.as_arc_identity()),
559                        delegations.clone(),
560                    )
561                ));
562
563                // Last check
564                let final_auth_check = self.is_authenticated();
565                eprintln!("Final check: is_authenticated() = {}", final_auth_check);
566            }
567        }
568
569        // create the idle manager on a successful login if we haven't disabled it
570        // and it doesn't already exist.
571        let disable_idle = match self.idle_options.as_ref() {
572            Some(options) => options.disable_idle.unwrap_or(false),
573            None => false,
574        };
575        if self.idle_manager.is_none() && !disable_idle {
576            let idle_manager_options = self.idle_options.as_ref().map(|o| o.idle_manager_options.clone());
577            let new_idle_manager = IdleManager::new(idle_manager_options);
578            self.idle_manager = Some(new_idle_manager);
579
580            // Register default callback only if idle_manager was successfully created
581            if let Some(idle_manager) = self.idle_manager.as_ref() {
582                Self::register_default_idle_callback(
583                    self.identity.clone(),
584                    self.storage.clone(),
585                    self.chain.clone(),
586                    Some(idle_manager.clone()),
587                    self.idle_options.clone(),
588                );
589            }
590        }
591
592        // on_success should be the last thing to do to avoid consumers
593        // interfering by navigating or refreshing the page
594        if let Some(on_success_cb) = on_success {
595            // Use try_lock to prevent blocking if the callback itself tries to re-enter AuthClient methods.
596            if let Ok(mut guard) = on_success_cb.try_lock() {
597                (*guard)(message);
598            } else {
599                eprintln!("Failed to acquire lock on on_success callback");
600            }
601        }
602
603        Ok(())
604    }
605
606    /// Returns the identity of the user.
607    pub fn identity(&self) -> Arc<dyn Identity> {
608        self.identity.as_arc_identity()
609    }
610
611    pub fn principal(&self) -> Result<Principal, String> {
612        self.identity().sender()
613    }
614
615    /// Checks if the user is authenticated.
616    pub fn is_authenticated(&self) -> bool {
617        let is_not_anonymous = self
618            .identity()
619            .sender()
620            .map(|s| s != Principal::anonymous())
621            .unwrap_or(false);
622
623        let has_chain = self.chain.lock().unwrap().is_some();
624
625        is_not_anonymous && has_chain
626    }
627
628    /// Logs the user in with default options.
629    pub fn login(&mut self) {
630        self.login_with_options(AuthClientLoginOptions::default());
631    }
632
633    /// Logs the user in with the provided options.
634    pub fn login_with_options(&mut self, options: AuthClientLoginOptions) {
635        // Reset completion flag for the new login attempt
636        self.login_complete.store(false, Ordering::SeqCst);
637
638        let window = web_sys::window().unwrap();
639
640        // Create the URL of the IDP. (e.g. https://XXXX/#authorize)
641        let identity_provider_url: web_sys::Url = options
642            .identity_provider
643            .clone()
644            .unwrap_or_else(|| web_sys::Url::new(IDENTITY_PROVIDER_DEFAULT).unwrap());
645
646        // Set the correct hash if it isn't already set.
647        identity_provider_url.set_hash(IDENTITY_PROVIDER_ENDPOINT);
648
649        // If `login` has been called previously, then close/remove any previous windows
650        // and event listeners.
651        if let Some(idp_window) = self.take_idp_window() {
652            // Ignore error if window is already closed
653            let _ = idp_window.close();
654        }
655        self.take_event_handler();
656
657        // Open a new window with the IDP provider.
658        let window_handle_result = window
659            .open_with_url_and_target_and_features(
660                &identity_provider_url.href(),
661                "idpWindow",
662                options.window_opener_features.as_deref().unwrap_or(""),
663            );
664
665        match window_handle_result {
666            Ok(Some(window_handle)) => {
667                self.set_idp_window(window_handle);
668
669                // Add an event listener to handle responses.
670                let handler = self.get_event_handler(identity_provider_url.clone(), options.clone());
671                self.set_event_handler(handler);
672
673                // Start checking for interruption, passing the completion flag
674                self.check_interruption(options.on_error.clone(), self.login_complete.clone());
675            }
676            Ok(None) => {
677                // Window opening was blocked by the browser (e.g., popup blocker)
678                self.login_complete.store(true, Ordering::SeqCst); // Mark as complete (failed)
679                if let Some(on_error) = options.on_error {
680                    on_error.lock().unwrap()(Some("Failed to open IdP window. Check popup blocker.".to_string()));
681                } else {
682                    eprintln!("Failed to open IdP window. Check popup blocker.");
683                }
684                // Clean up potentially stored (but unused) handler/window refs for this ID
685                self.take_event_handler();
686                self.take_idp_window();
687            }
688            Err(e) => {
689                 // Other error during window opening
690                self.login_complete.store(true, Ordering::SeqCst); // Mark as complete (failed)
691                let error_message = format!("Error opening IdP window: {:?}", e);
692                if let Some(on_error) = options.on_error {
693                    on_error.lock().unwrap()(Some(error_message.clone()));
694                } else {
695                    eprintln!("{}", error_message);
696                }
697                // Clean up potentially stored (but unused) handler/window refs for this ID
698                self.take_event_handler();
699                self.take_idp_window();
700            }
701        }
702    }
703
704    /// Checks for user interruption during the login process.
705    fn check_interruption(&self, on_error: Option<OnError>, login_complete: Arc<AtomicBool>) {
706        let client_id = *self.id;
707        let idp_window = self.get_idp_window();
708        let login_complete_clone = login_complete.clone();
709
710        spawn_local({
711            async move {
712                if let Some(idp_window_ref) = idp_window {
713                    // Give the authentication process a moment to start before checking for interruptions
714                    sleep(1000).await;
715
716                    // Check periodically if the window is still open
717                    while !idp_window_ref.closed().unwrap_or(true)
718                        && !login_complete_clone.load(Ordering::SeqCst)
719                    {
720                        sleep(INTERRUPT_CHECK_INTERVAL).await;
721                    }
722
723                    // Only report a user interrupt if login isn't already complete AND the window is closed
724                    // This avoids false UserInterrupt errors when the window is closed after authentication completes
725                    if idp_window_ref.closed().unwrap_or(true) && !login_complete_clone.load(Ordering::SeqCst) {
726                        // Clean up resources first
727                        let _ = idp_window_ref.close(); // Ignore error if already closed
728
729                        // Remove the event handler from thread-local storage
730                        EVENT_HANDLERS.with(|cell| {
731                            // Use try_borrow_mut to avoid panic if already borrowed
732                            if let Ok(mut map) = cell.try_borrow_mut() {
733                                map.remove(&client_id);
734                            } else {
735                                eprintln!("AuthClient::check_interruption: Could not remove event handler for id {} (already borrowed)", client_id);
736                            }
737                        });
738
739                        // Also remove the window reference if it wasn't removed by handle_success/handle_failure
740                        IDP_WINDOWS.with(|cell| {
741                            if let Ok(mut map) = cell.try_borrow_mut() {
742                                map.remove(&client_id);
743                            } else {
744                                eprintln!("AuthClient::check_interruption: Could not remove IDP window for id {} (already borrowed)", client_id);
745                            }
746                        });
747
748                        // Double-check one last time before triggering the error callback
749                        // This helps avoid race conditions where login completion happens right as we're checking
750                        if !login_complete_clone.load(Ordering::SeqCst) {
751                            // Only now call the error callback if provided
752                            if let Some(on_error) = on_error {
753                                // Ensure login_complete is set before calling the callback
754                                login_complete_clone.store(true, Ordering::SeqCst);
755                                on_error.lock().unwrap()(Some(ERROR_USER_INTERRUPT.to_string()));
756                            } else {
757                                // If no error handler, still mark as complete to prevent potential issues
758                                login_complete_clone.store(true, Ordering::SeqCst);
759                            }
760                        }
761                    } else {
762                        // Window is not closed or login is already complete, no need to do anything
763                        // Resources will be cleaned up by handle_success/failure or another mechanism
764                    }
765                }
766            }
767        });
768    }
769
770    /// Returns an event handler for the login process.
771    fn get_event_handler(
772        &mut self,
773        identity_provider_url: web_sys::Url,
774        options: AuthClientLoginOptions,
775    ) -> EventListener {
776        let client = self.clone();
777
778        let callback = move |event: &web_sys::Event| {
779            let event = event.dyn_ref::<MessageEvent>().unwrap();
780
781            if event.origin() != identity_provider_url.origin() {
782                // Ignore any event that is not from the identity provider
783                return;
784            }
785
786            let message = from_value::<IdentityServiceResponseMessage>(event.data()).map_err(|e| e.to_string());
787
788            let max_time_to_live = options
789                .max_time_to_live
790                .unwrap_or(Self::DEFAULT_TIME_TO_LIVE);
791
792            let handle_error_wrapper = |error: String| {
793                // Clone necessary parts, avoid cloning the whole client into the async block if possible
794                let login_complete = client.login_complete.clone();
795                let on_error = options.on_error.clone();
796                let client_id = *client.id; // Get the ID value
797
798                spawn_local(async move {
799                    // Signal completion
800                    login_complete.store(true, Ordering::SeqCst);
801
802                    // Clean up window and handler (using the ID)
803                    if let Some(window) = IDP_WINDOWS.with(|map| map.borrow_mut().remove(&client_id)) {
804                        let _ = window.close();
805                    }
806                    EVENT_HANDLERS.with(|map| map.borrow_mut().remove(&client_id));
807
808                    // Call the error callback
809                    if let Some(on_error_cb) = on_error {
810                        if let Ok(mut guard) = on_error_cb.try_lock() {
811                            (*guard)(Some(error));
812                        } else {
813                            eprintln!("Failed to acquire lock on on_error callback in event handler");
814                        }
815                    } else {
816                         eprintln!("AuthClient login failed in event handler: {}", error);
817                    }
818                });
819            };
820
821            match message.and_then(|m| m.kind()) {
822                Ok(kind) => match kind {
823                    IdentityServiceResponseKind::Ready => {
824                        use web_sys::js_sys::{Reflect, Uint8Array};
825
826                        let request = InternetIdentityAuthRequest {
827                            kind: "authorize-client".to_string(),
828                            session_public_key: client
829                                .key
830                                .public_key()
831                                .expect("Failed to get public key"),
832                            max_time_to_live: Some(max_time_to_live),
833                            allow_pin_authentication: options.allow_pin_authentication,
834                            derivation_origin: options
835                                .derivation_origin
836                                .clone()
837                                .map(|d| d.to_string().into()),
838                        };
839                        let request_js_value = match JsValue::from_serde(&request) {
840                            Ok(value) => value,
841                            Err(err) => {
842                                handle_error_wrapper(format!("Failed to serialize request: {}", err));
843                                return;
844                            }
845                        };
846
847                        let session_public_key_js = Uint8Array::from(&request.session_public_key[..]).into();
848                        if Reflect::set(
849                            &request_js_value,
850                            &JsValue::from_str("sessionPublicKey"),
851                            &session_public_key_js,
852                        ).is_err() {
853                            handle_error_wrapper("Failed to set sessionPublicKey on request".to_string());
854                            return;
855                        }
856
857                        if let Some(custom_values) = options.custom_values.clone() {
858                            for (k, v) in custom_values {
859                                match JsValue::from_serde(&v) {
860                                    Ok(value) => {
861                                        if Reflect::set(&request_js_value, &JsValue::from_str(&k), &value).is_err() {
862                                            handle_error_wrapper(format!("Failed to set custom value '{}'", k));
863                                        }
864                                    }
865                                    Err(err) => {
866                                        handle_error_wrapper(format!("Failed to serialize custom value '{}': {}", k, err));
867                                    }
868                                }
869                            }
870                        }
871
872                        if let Some(idp_window) = client.get_idp_window() {
873                            if idp_window
874                                .post_message(&request_js_value, &identity_provider_url.origin())
875                                .is_err() {
876                                    handle_error_wrapper("Failed to post message to IdP window".to_string());
877                                }
878                        } else {
879                            // This case might happen if the window was closed unexpectedly between checks
880                            handle_error_wrapper("IdP window not found when trying to post message".to_string());
881                        }
882                    }
883                    IdentityServiceResponseKind::AuthSuccess(response) => {
884                        let mut client_clone = client.clone();
885                        let on_success = options.on_success.clone();
886                        let on_error = options.on_error.clone();
887                        spawn_local(async move {
888                            if let Err(e) = client_clone.handle_success(response, on_success).await {
889                                // Handle potential errors from handle_success itself
890                                eprintln!("Error during handle_success: {}", e);
891                                // Optionally call on_error here as well
892                                if let Some(on_error_cb) = on_error {
893                                    if let Ok(mut guard) = on_error_cb.try_lock() {
894                                        (*guard)(Some(format!("Error processing successful login: {:?}", e)));
895                                    }
896                                }
897                            }
898                        });
899                    }
900                    IdentityServiceResponseKind::AuthFailure(error_message) => {
901                        handle_error_wrapper(error_message);
902                    }
903                },
904                Err(e) => {
905                    handle_error_wrapper(e);
906                }
907            }
908        };
909
910        EventListener::new(&window(), "message", callback)
911    }
912
913    /// Logs out the user and clears the stored identity.
914    async fn logout_core(
915        identity: &mut ArcIdentity,
916        mut storage: AuthClientStorageType,
917        chain: Arc<Mutex<Option<DelegationChain>>>,
918        return_to: Option<Location>,
919    ) {
920        Self::delete_storage(&mut storage).await;
921
922        // Reset this auth client to a non-authenticated state.
923        *identity = ArcIdentity::Anonymous(Arc::new(AnonymousIdentity));
924        if let Ok(mut guard) = chain.try_lock() {
925            guard.take();
926        } else {
927            eprintln!("Failed to acquire lock on delegation chain during logout");
928        }
929
930        // If a return URL is provided, redirect the user to that URL.
931        if let Some(return_to) = return_to {
932            if let Some(window) = web_sys::window() {
933                let href_result = return_to.href();
934                if let Ok(href) = href_result {
935                    if let Ok(history) = window.history() {
936                        if history.push_state_with_url(&JsValue::null(), "", Some(&href)).is_err() && window.location().set_href(&href).is_err() {
937                            eprintln!("Failed to set href during logout");
938                        }
939                    } else if window.location().set_href(&href).is_err() {
940                        eprintln!("Failed to set href during logout (no history)");
941                    }
942                } else {
943                    eprintln!("Failed to get href from return_to location during logout");
944                }
945            }
946        }
947    }
948
949    /// Log the user out.
950    /// If a return URL is provided, the user will be redirected to that URL after logging out.
951    pub async fn logout(&mut self, return_to: Option<Location>) {
952        if let Some(idle_manager) = self.idle_manager.take() {
953            drop(idle_manager);
954        }
955
956        Self::logout_core(
957            &mut self.identity,
958            self.storage.clone(),
959            self.chain.clone(),
960            return_to,
961        )
962        .await;
963    }
964
965    /// Deletes the stored keys from the provided storage.
966    async fn delete_storage<S>(storage: &mut S)
967    where
968        S: AuthClientStorage,
969    {
970        storage.remove(KEY_STORAGE_KEY).await;
971        storage.remove(KEY_STORAGE_DELEGATION).await;
972        storage.remove(KEY_VECTOR).await;
973    }
974}
975
976/// Builder for the [`AuthClient`].
977#[derive(Default)]
978pub struct AuthClientBuilder {
979    identity: Option<ArcIdentity>,
980    storage: Option<AuthClientStorageType>,
981    key_type: Option<BaseKeyType>,
982    idle_options: Option<IdleOptions>,
983}
984
985impl AuthClientBuilder {
986    /// Creates a new [`AuthClientBuilder`].
987    fn new() -> Self {
988        Self::default()
989    }
990
991    /// An optional identity to use as the base. If not provided, an `Ed25519` key pair will be used.
992    pub fn identity(mut self, identity: ArcIdentity) -> Self {
993        self.identity = Some(identity);
994        self
995    }
996
997    /// Optional storage with get, set, and remove methods. Currentry only `LocalStorage` is supported.
998    pub fn storage(mut self, storage: AuthClientStorageType) -> Self {
999        self.storage = Some(storage);
1000        self
1001    }
1002
1003    /// The type of key to use for the base key. If not provided, `Ed25519` will be used by default.
1004    pub fn key_type(mut self, key_type: BaseKeyType) -> Self {
1005        self.key_type = Some(key_type);
1006        self
1007    }
1008
1009    /// Options for handling idle timeouts. If not provided, default options will be used.
1010    pub fn idle_options(mut self, idle_options: IdleOptions) -> Self {
1011        self.idle_options = Some(idle_options);
1012        self
1013    }
1014
1015    // --- Methods to configure IdleOptions directly on the builder ---
1016
1017    /// Helper to get mutable access to idle_options, creating default if None.
1018    fn idle_options_mut(&mut self) -> &mut IdleOptions {
1019        self.idle_options.get_or_insert_with(IdleOptions::default)
1020    }
1021
1022    /// If set to `true`, disables the idle timeout functionality.
1023    pub fn disable_idle(mut self, disable_idle: bool) -> Self {
1024        self.idle_options_mut().disable_idle = Some(disable_idle);
1025        self
1026    }
1027
1028    /// If set to `true`, disables the default idle timeout callback.
1029    pub fn disable_default_idle_callback(mut self, disable_default_idle_callback: bool) -> Self {
1030        self.idle_options_mut().disable_default_idle_callback = Some(disable_default_idle_callback);
1031        self
1032    }
1033
1034    /// Options for the [`IdleManager`] that handles idle timeouts.
1035    pub fn idle_manager_options(mut self, idle_manager_options: IdleManagerOptions) -> Self {
1036        self.idle_options_mut().idle_manager_options = idle_manager_options;
1037        self
1038    }
1039
1040    /// A callback function to be executed when the system becomes idle.
1041    /// Note: This replaces any existing callbacks. Use `add_on_idle` for multiple.
1042    pub fn on_idle(mut self, on_idle: fn()) -> Self {
1043        self.idle_options_mut().idle_manager_options.on_idle = Arc::new(Mutex::new(vec![Box::new(on_idle) as Box<dyn FnMut() + Send>]));
1044        self
1045    }
1046
1047    /// Adds a callback function to be executed when the system becomes idle.
1048    pub fn add_on_idle<F>(mut self, on_idle: F) -> Self
1049    where
1050        F: FnMut() + Send + 'static,
1051    {
1052        let options = self.idle_options_mut();
1053        // Ensure the Arc<Mutex<Vec>> exists
1054        if Arc::strong_count(&options.idle_manager_options.on_idle) == 0 {
1055             // This case should ideally not happen if initialized correctly, but handle defensively
1056             options.idle_manager_options.on_idle = Arc::new(Mutex::new(Vec::new()));
1057        }
1058        // Add the new callback
1059        if let Ok(mut guard) = options.idle_manager_options.on_idle.lock() {
1060            guard.push(Box::new(on_idle));
1061        } else {
1062             eprintln!("Failed to lock on_idle callbacks to add new one.");
1063        }
1064        self
1065    }
1066
1067    /// The duration of inactivity after which the system is considered idle.
1068    pub fn idle_timeout(mut self, idle_timeout: u32) -> Self {
1069        self.idle_options_mut().idle_manager_options.idle_timeout = Some(idle_timeout);
1070        self
1071    }
1072
1073    /// A delay for debouncing scroll events.
1074    pub fn scroll_debounce(mut self, scroll_debounce: u32) -> Self {
1075        self.idle_options_mut().idle_manager_options.scroll_debounce = Some(scroll_debounce);
1076        self
1077    }
1078
1079    /// A flag indicating whether to capture scroll events.
1080    pub fn capture_scroll(mut self, capture_scroll: bool) -> Self {
1081        self.idle_options_mut().idle_manager_options.capture_scroll = Some(capture_scroll);
1082        self
1083    }
1084
1085    /// Builds a new [`AuthClient`].
1086    pub async fn build(self) -> Result<AuthClient, DelegationError> {
1087        let options = AuthClientCreateOptions {
1088            identity: self.identity,
1089            storage: self.storage,
1090            key_type: self.key_type,
1091            idle_options: self.idle_options,
1092        };
1093
1094        AuthClient::new_with_options(options).await
1095    }
1096}
1097
1098#[derive(Clone)]
1099pub enum ArcIdentity {
1100    Anonymous(Arc<AnonymousIdentity>),
1101    Ed25519(Arc<BasicIdentity>),
1102    Delegated(Arc<DelegatedIdentity>),
1103}
1104
1105impl Default for ArcIdentity {
1106    fn default() -> Self {
1107        ArcIdentity::Anonymous(Arc::new(AnonymousIdentity))
1108    }
1109}
1110
1111impl fmt::Debug for ArcIdentity {
1112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1113        match self {
1114            ArcIdentity::Anonymous(_) => write!(f, "ArcIdentity::Anonymous"),
1115            ArcIdentity::Ed25519(_) => write!(f, "ArcIdentity::Ed25519"),
1116            ArcIdentity::Delegated(_) => write!(f, "ArcIdentity::Delegated"),
1117        }
1118    }
1119}
1120
1121impl ArcIdentity {
1122    fn as_arc_identity(&self) -> Arc<dyn Identity> {
1123        match self {
1124            ArcIdentity::Anonymous(id) => id.clone(),
1125            ArcIdentity::Ed25519(id) => id.clone(),
1126            ArcIdentity::Delegated(id) => id.clone(),
1127        }
1128    }
1129
1130    fn public_key(&self) -> Option<Vec<u8>> {
1131        match self {
1132            ArcIdentity::Anonymous(id) => id.public_key(),
1133            ArcIdentity::Ed25519(id) => id.public_key(),
1134            ArcIdentity::Delegated(id) => id.public_key(),
1135        }
1136    }
1137}
1138
1139impl From<AnonymousIdentity> for ArcIdentity {
1140    fn from(identity: AnonymousIdentity) -> Self {
1141        ArcIdentity::Anonymous(Arc::new(identity))
1142    }
1143}
1144
1145impl From<BasicIdentity> for ArcIdentity {
1146    fn from(identity: BasicIdentity) -> Self {
1147        ArcIdentity::Ed25519(Arc::new(identity))
1148    }
1149}
1150
1151impl From<DelegatedIdentity> for ArcIdentity {
1152    fn from(identity: DelegatedIdentity) -> Self {
1153        ArcIdentity::Delegated(Arc::new(identity))
1154    }
1155}
1156
1157/// Options for the [`AuthClient::login_with_options`].
1158#[derive(Clone, Default)]
1159pub struct AuthClientLoginOptions {
1160    /// The URL of the identity provider.
1161    identity_provider: Option<web_sys::Url>,
1162
1163    /// Expiration of the authentication in nanoseconds.
1164    max_time_to_live: Option<u64>,
1165
1166    /// If present, indicates whether or not the Identity Provider should allow the user to authenticate and/or register using a temporary key/PIN identity.
1167    ///
1168    /// Authenticating dapps may want to prevent users from using Temporary keys/PIN identities because Temporary keys/PIN identities are less secure than Passkeys (webauthn credentials) and because Temporary keys/PIN identities generally only live in a browser database (which may get cleared by the browser/OS).
1169    allow_pin_authentication: Option<bool>,
1170
1171    /// Origin for Identity Provider to use while generating the delegated identity. For II, the derivation origin must authorize this origin by setting a record at `<derivation-origin>/.well-known/ii-alternative-origins`.
1172    ///
1173    /// See: <https://github.com/dfinity/internet-identity/blob/main/docs/ii-spec.mdx#alternative-frontend-origins>
1174    derivation_origin: Option<web_sys::Url>,
1175
1176    /// Auth Window feature config string.
1177    ///
1178    /// # Example
1179    /// ```
1180    /// toolbar=0,location=0,menubar=0,width=500,height=500,left=100,top=100
1181    /// ```
1182    window_opener_features: Option<String>,
1183
1184    /// Callback once login has completed.
1185    on_success: Option<OnSuccess>,
1186
1187    /// Callback in case authentication fails.
1188    on_error: Option<OnError>,
1189
1190    /// Extra values to be passed in the login request during the authorize-ready phase.
1191    custom_values: Option<HashMap<String, serde_json::Value>>,
1192}
1193
1194impl AuthClientLoginOptions {
1195    /// Creates a new [`AuthClientLoginOptionsBuilder`].
1196    pub fn builder() -> AuthClientLoginOptionsBuilder {
1197        AuthClientLoginOptionsBuilder::new()
1198    }
1199}
1200
1201/// Builder for the [`AuthClientLoginOptions`].
1202pub struct AuthClientLoginOptionsBuilder {
1203    identity_provider: Option<web_sys::Url>,
1204    max_time_to_live: Option<u64>,
1205    allow_pin_authentication: Option<bool>,
1206    derivation_origin: Option<web_sys::Url>,
1207    window_opener_features: Option<String>,
1208    on_success: Option<Box<dyn FnMut(AuthResponseSuccess) + Send>>,
1209    on_error: Option<Box<dyn FnMut(Option<String>) + Send>>,
1210    custom_values: Option<HashMap<String, serde_json::Value>>,
1211}
1212
1213impl AuthClientLoginOptionsBuilder {
1214    fn new() -> Self {
1215        Self {
1216            identity_provider: None,
1217            max_time_to_live: None,
1218            allow_pin_authentication: None,
1219            derivation_origin: None,
1220            window_opener_features: None,
1221            on_success: None,
1222            on_error: None,
1223            custom_values: None,
1224        }
1225    }
1226
1227    /// The URL of the identity provider.
1228    pub fn identity_provider(mut self, identity_provider: web_sys::Url) -> Self {
1229        self.identity_provider = Some(identity_provider);
1230        self
1231    }
1232
1233    /// Expiration of the authentication in nanoseconds.
1234    pub fn max_time_to_live(mut self, max_time_to_live: u64) -> Self {
1235        self.max_time_to_live = Some(max_time_to_live);
1236        self
1237    }
1238
1239    /// If present, indicates whether or not the Identity Provider should allow the user to authenticate and/or register using a temporary key/PIN identity.
1240    ///
1241    /// Authenticating dapps may want to prevent users from using Temporary keys/PIN identities because Temporary keys/PIN identities are less secure than Passkeys (webauthn credentials) and because Temporary keys/PIN identities generally only live in a browser database (which may get cleared by the browser/OS).
1242    pub fn allow_pin_authentication(mut self, allow_pin_authentication: bool) -> Self {
1243        self.allow_pin_authentication = Some(allow_pin_authentication);
1244        self
1245    }
1246
1247    /// Origin for Identity Provider to use while generating the delegated identity. For II, the derivation origin must authorize this origin by setting a record at `<derivation-origin>/.well-known/ii-alternative-origins`.
1248    ///
1249    /// See: <https://github.com/dfinity/internet-identity/blob/main/docs/ii-spec.mdx#alternative-frontend-origins>
1250    pub fn derivation_origin(mut self, derivation_origin: web_sys::Url) -> Self {
1251        self.derivation_origin = Some(derivation_origin);
1252        self
1253    }
1254
1255    /// Auth Window feature config string.
1256    ///
1257    /// # Example
1258    /// ```
1259    /// toolbar=0,location=0,menubar=0,width=500,height=500,left=100,top=100
1260    /// ```
1261    pub fn window_opener_features(mut self, window_opener_features: String) -> Self {
1262        self.window_opener_features = Some(window_opener_features);
1263        self
1264    }
1265
1266    /// Callback once login has completed.
1267    pub fn on_success<F>(mut self, on_success: F) -> Self
1268    where
1269        F: FnMut(AuthResponseSuccess) + Send + 'static,
1270    {
1271        self.on_success = Some(Box::new(on_success));
1272        self
1273    }
1274
1275    /// Callback in case authentication fails.
1276    pub fn on_error<F>(mut self, on_error: F) -> Self
1277    where
1278        F: FnMut(Option<String>) + Send + 'static,
1279    {
1280        self.on_error = Some(Box::new(on_error));
1281        self
1282    }
1283
1284    /// Extra values to be passed in the login request during the authorize-ready phase.
1285    pub fn custom_values(mut self, custom_values: HashMap<String, serde_json::Value>) -> Self {
1286        self.custom_values = Some(custom_values);
1287        self
1288    }
1289
1290    /// Build the [`AuthClientLoginOptions`].
1291    pub fn build(self) -> AuthClientLoginOptions {
1292        AuthClientLoginOptions {
1293            identity_provider: self.identity_provider,
1294            max_time_to_live: self.max_time_to_live,
1295            allow_pin_authentication: self.allow_pin_authentication,
1296            derivation_origin: self.derivation_origin,
1297            window_opener_features: self.window_opener_features,
1298            on_success: self.on_success.map(|f| Arc::new(Mutex::new(f))),
1299            on_error: self.on_error.map(|f| Arc::new(Mutex::new(f))),
1300            custom_values: self.custom_values,
1301        }
1302    }
1303}
1304
1305/// Options for creating a new [`AuthClient`].
1306#[derive(Default, Clone)]
1307pub struct AuthClientCreateOptions {
1308    /// An optional identity to use as the base. If not provided, an `Ed25519` key pair will be used.
1309    pub identity: Option<ArcIdentity>,
1310    /// Optional storage with get, set, and remove methods. Currentry only `LocalStorage` is supported.
1311    pub storage: Option<AuthClientStorageType>,
1312    /// The type of key to use for the base key. If not provided, `Ed25519` will be used by default.
1313    pub key_type: Option<BaseKeyType>,
1314    /// Options for handling idle timeouts. If not provided, default options will be used.
1315    pub idle_options: Option<IdleOptions>,
1316}
1317
1318/// Options for handling idle timeouts.
1319#[derive(Default, Clone, Debug)]
1320pub struct IdleOptions {
1321    /// If set to `true`, disables the idle timeout functionality.
1322    pub disable_idle: Option<bool>,
1323    /// If set to `true`, disables the default idle timeout callback.
1324    pub disable_default_idle_callback: Option<bool>,
1325    /// Options for the [`IdleManager`] that handles idle timeouts.
1326    pub idle_manager_options: IdleManagerOptions,
1327}
1328
1329impl IdleOptions {
1330    /// Create a new [`IdleOptionsBuilder`].
1331    pub fn builder() -> IdleOptionsBuilder {
1332        IdleOptionsBuilder::new()
1333    }
1334}
1335
1336/// Builder for [`IdleOptions`].
1337pub struct IdleOptionsBuilder {
1338    disable_idle: Option<bool>,
1339    disable_default_idle_callback: Option<bool>,
1340    idle_manager_options: IdleManagerOptions,
1341}
1342
1343impl IdleOptionsBuilder {
1344    fn new() -> Self {
1345        Self {
1346            disable_idle: None,
1347            disable_default_idle_callback: None,
1348            idle_manager_options: IdleManagerOptions::default(),
1349        }
1350    }
1351
1352    /// If set to `true`, disables the idle timeout functionality.
1353    pub fn disable_idle(mut self, disable_idle: bool) -> Self {
1354        self.disable_idle = Some(disable_idle);
1355        self
1356    }
1357
1358    /// If set to `true`, disables the default idle timeout callback.
1359    pub fn disable_default_idle_callback(mut self, disable_default_idle_callback: bool) -> Self {
1360        self.disable_default_idle_callback = Some(disable_default_idle_callback);
1361        self
1362    }
1363
1364    /// Options for the [`IdleManager`] that handles idle timeouts.
1365    pub fn idle_manager_options(mut self, idle_manager_options: IdleManagerOptions) -> Self {
1366        self.idle_manager_options = idle_manager_options;
1367        self
1368    }
1369
1370    /// A callback function to be executed when the system becomes idle.
1371    /// Note: This replaces any existing callbacks. Use `add_on_idle` for multiple.
1372    pub fn on_idle(mut self, on_idle: fn()) -> Self {
1373        self.idle_manager_options.on_idle = Arc::new(Mutex::new(vec![Box::new(on_idle) as Box<dyn FnMut() + Send>]));
1374        self
1375    }
1376
1377    /// Adds a callback function to be executed when the system becomes idle.
1378    pub fn add_on_idle<F>(mut self, on_idle: F) -> Self
1379    where
1380        F: FnMut() + Send + 'static,
1381    {
1382        // Ensure the Arc<Mutex<Vec>> exists
1383        if Arc::strong_count(&self.idle_manager_options.on_idle) == 0 {
1384             // This case should ideally not happen if initialized correctly, but handle defensively
1385             self.idle_manager_options.on_idle = Arc::new(Mutex::new(Vec::new()));
1386        }
1387        // Add the new callback
1388        if let Ok(mut guard) = self.idle_manager_options.on_idle.lock() {
1389            guard.push(Box::new(on_idle));
1390        } else {
1391             eprintln!("Failed to lock on_idle callbacks to add new one.");
1392        }
1393        self
1394    }
1395
1396    /// The duration of inactivity after which the system is considered idle.
1397    pub fn idle_timeout(mut self, idle_timeout: u32) -> Self {
1398        self.idle_manager_options.idle_timeout = Some(idle_timeout);
1399        self
1400    }
1401
1402    /// A delay for debouncing scroll events.
1403    pub fn scroll_debounce(mut self, scroll_debounce: u32) -> Self {
1404        self.idle_manager_options.scroll_debounce = Some(scroll_debounce);
1405        self
1406    }
1407
1408    /// A flag indicating whether to capture scroll events.
1409    pub fn capture_scroll(mut self, capture_scroll: bool) -> Self {
1410        self.idle_manager_options.capture_scroll = Some(capture_scroll);
1411        self
1412    }
1413
1414    /// Build the [`IdleOptions`].
1415    pub fn build(self) -> IdleOptions {
1416        IdleOptions {
1417            disable_idle: self.disable_idle,
1418            disable_default_idle_callback: self.disable_default_idle_callback,
1419            idle_manager_options: self.idle_manager_options,
1420        }
1421    }
1422}
1423
1424/// Enum representing the type of base key used for the identity.
1425///
1426/// Currently, only Ed25519 is supported.
1427#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
1428pub enum BaseKeyType {
1429    /// Ed25519 base key type.
1430    #[default]
1431    Ed25519,
1432}
1433
1434impl fmt::Display for BaseKeyType {
1435    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1436        match self {
1437            BaseKeyType::Ed25519 => write!(f, "{}", ED25519_KEY_LABEL),
1438        }
1439    }
1440}
1441
1442#[allow(dead_code)]
1443#[cfg(test)]
1444mod tests {
1445    use super::*;
1446    use wasm_bindgen_test::*;
1447
1448    #[wasm_bindgen_test]
1449    fn test_idle_options_builder() {
1450        let options = IdleOptionsBuilder::new()
1451            .disable_idle(true)
1452            .disable_default_idle_callback(true)
1453            .on_idle(|| {})
1454            .idle_timeout(1000)
1455            .scroll_debounce(500)
1456            .capture_scroll(true)
1457            .build();
1458        assert_eq!(options.disable_idle, Some(true));
1459        assert_eq!(options.disable_default_idle_callback, Some(true));
1460        assert!(options.idle_manager_options.on_idle.as_ref().lock().unwrap().is_empty());
1461        assert_eq!(options.idle_manager_options.idle_timeout, Some(1000));
1462        assert_eq!(options.idle_manager_options.scroll_debounce, Some(500));
1463        assert_eq!(options.idle_manager_options.capture_scroll, Some(true));
1464    }
1465
1466    #[wasm_bindgen_test]
1467    fn test_base_key_type_display() {
1468        assert_eq!(BaseKeyType::Ed25519.to_string(), ED25519_KEY_LABEL);
1469    }
1470
1471    #[wasm_bindgen_test]
1472    fn test_base_key_type_default() {
1473        assert_eq!(BaseKeyType::default(), BaseKeyType::Ed25519);
1474    }
1475
1476    #[wasm_bindgen_test]
1477    async fn test_auth_client_builder() {
1478        let private_key = SigningKey::new(thread_rng());
1479        let identity = ArcIdentity::Ed25519(Arc::new(BasicIdentity::from_signing_key(private_key)));
1480
1481        let idle_options = IdleOptions::builder()
1482            .disable_idle(true)
1483            .disable_default_idle_callback(true)
1484            .on_idle(|| {})
1485            .idle_timeout(1000)
1486            .scroll_debounce(500)
1487            .capture_scroll(true)
1488            .build();
1489
1490        let auth_client = AuthClient::builder()
1491            .identity(identity.clone())
1492            .idle_options(idle_options)
1493            .build()
1494            .await
1495            .unwrap();
1496
1497        assert!(!auth_client.is_authenticated());
1498        assert_eq!(auth_client.identity().sender().unwrap(), identity.as_arc_identity().sender().unwrap()); // Check if identity was set
1499    }
1500
1501    #[wasm_bindgen_test]
1502    fn test_auth_client_login_options_builder() {
1503        let custom_values = vec![("key".to_string(), "value".into())].into_iter().collect();
1504
1505        let options = AuthClientLoginOptions::builder()
1506            .allow_pin_authentication(true)
1507            .custom_values(custom_values)
1508            .on_error(|_| {})
1509            .on_success(|_| {})
1510            .build();
1511
1512        assert_eq!(options.allow_pin_authentication, Some(true));
1513        assert!(options.on_error.is_some());
1514        assert!(options.on_success.is_some());
1515        assert!(options.custom_values.is_some());
1516    }
1517}