Skip to main content

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