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