1use 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;
64pub const ERROR_USER_INTERRUPT: &str = "UserInterrupt";
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72#[serde(rename_all = "camelCase")]
73struct InternetIdentityAuthRequest {
74 pub kind: String,
76 pub session_public_key: Vec<u8>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub max_time_to_live: Option<u64>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub allow_pin_authentication: Option<bool>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub derivation_origin: Option<String>,
87}
88
89#[derive(Debug, Clone)]
94pub struct AuthResponseSuccess {
95 pub delegations: Vec<SignedDelegation>,
97 pub user_public_key: Vec<u8>,
99 pub authn_method: String,
101}
102
103#[derive(Debug, Clone, Deserialize)]
109#[serde(rename_all = "camelCase")]
110struct IdentityServiceResponseMessage {
111 kind: String,
113
114 delegations: Option<Vec<SignedDelegation>>,
116
117 user_public_key: Option<Vec<u8>>,
119
120 authn_method: Option<String>,
122
123 text: Option<String>,
125}
126
127impl IdentityServiceResponseMessage {
128 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#[derive(Debug, Clone)]
149enum IdentityServiceResponseKind {
150 Ready,
152 AuthSuccess(AuthResponseSuccess),
154 AuthFailure(String),
156}
157
158#[derive(Clone, Debug)]
161pub struct AuthClient {
162 identity: Arc<Mutex<ArcIdentity>>,
164 key: Key,
166 storage: AuthClientStorageType,
168 chain: Arc<Mutex<Option<DelegationChain>>>,
170 pub idle_manager: Option<IdleManager>,
172 idle_options: Option<IdleOptions>,
174 id: Arc<usize>,
177 login_complete: Arc<AtomicBool>,
179}
180
181impl Drop for AuthClient {
182 fn drop(&mut self) {
183 if Arc::strong_count(&self.id) == 1 {
185 let id_val = *self.id; 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 if let Some(window) = map.remove(&id_val) {
205 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 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 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 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 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 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 const DEFAULT_TIME_TO_LIVE: u64 = 8 * 60 * 60 * 1_000_000_000;
262
263 pub fn builder() -> AuthClientBuilder {
265 AuthClientBuilder::new()
266 }
267
268 pub async fn new() -> Result<Self, DelegationError> {
270 Self::new_with_options(AuthClientCreateOptions::default()).await
271 }
272
273 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 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 let chain_result = DelegationChain::from_json(&chain_storage);
312 chain = Arc::new(Mutex::new(Some(chain_result)));
313
314 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 let public_key = chain_inner.public_key.clone();
321 let delegations = chain_inner.delegations.clone();
322 Some((public_key, delegations))
323 } else {
324 None
326 }
327 } else {
328 Some((Vec::new(), Vec::new()))
330 }
331 } else {
332 Some((Vec::new(), Vec::new()))
334 }
335 };
336
337 match delegation_data {
339 Some((public_key, delegations)) => {
340 if !public_key.is_empty() {
341 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 #[cfg(feature = "tracing")]
354 info!(
355 "Found invalid delegation chain in storage - clearing credentials"
356 );
357 Self::delete_storage(&mut storage).await;
358
359 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 let id = {
385 static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
386 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 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 async fn handle_success(
449 &mut self,
450 message: AuthResponseSuccess,
451 on_success: Option<OnSuccess>,
452 ) -> Result<(), DelegationError> {
453 self.login_complete.store(true, Ordering::SeqCst);
456
457 if let Some(w) = self.take_idp_window() {
459 let _ = w.close();
461 };
462 self.take_event_handler(); let delegations = message.delegations.clone();
465 let user_public_key = message.user_public_key.clone();
466
467 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 let chain_json = delegation_chain.to_json();
482
483 let _ = self
486 .storage
487 .set(KEY_STORAGE_DELEGATION, chain_json.clone())
488 .await;
489
490 {
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 let is_auth = self.is_authenticated();
513 if !is_auth {
514 #[cfg(feature = "tracing")]
517 warn!("CRITICAL: is_authenticated() returned false after successful login");
518
519 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 if let Ok(mut guard) = self.chain.lock() {
541 *guard = Some(DelegationChain::from_json(&chain_json));
542 }
543
544 let is_auth_retry = self.is_authenticated();
546 #[cfg(feature = "tracing")]
547 debug!("After fix attempt: is_authenticated() = {}", is_auth_retry);
548
549 if !is_auth_retry {
552 if let Ok(_principal) = self.identity().sender() {
553 #[cfg(feature = "tracing")]
554 debug!("Current principal: {}", _principal);
555 }
556
557 {
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 let _final_auth_check = self.is_authenticated();
574 #[cfg(feature = "tracing")]
575 debug!("Final check: is_authenticated() = {}", _final_auth_check);
576 }
577 }
578
579 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 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 if let Some(on_success_cb) = on_success {
608 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 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 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 pub fn login(&mut self) {
659 self.login_with_options(AuthClientLoginOptions::default());
660 }
661
662 pub fn login_with_options(&mut self, options: AuthClientLoginOptions) {
664 self.login_complete.store(false, Ordering::SeqCst);
666
667 let window = web_sys::window().unwrap();
668
669 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 identity_provider_url.set_hash(IDENTITY_PROVIDER_ENDPOINT);
677
678 if let Some(idp_window) = self.take_idp_window() {
681 let _ = idp_window.close();
683 }
684 self.take_event_handler();
685
686 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 let handler =
699 self.get_event_handler(identity_provider_url.clone(), options.clone());
700 self.set_event_handler(handler);
701
702 self.check_interruption(options.on_error.clone(), self.login_complete.clone());
704 }
705 Ok(None) => {
706 self.login_complete.store(true, Ordering::SeqCst); 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 self.take_event_handler();
723 self.take_idp_window();
724 }
725 Err(e) => {
726 self.login_complete.store(true, Ordering::SeqCst); 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 self.take_event_handler();
742 self.take_idp_window();
743 }
744 }
745 }
746
747 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 sleep(1000).await;
758
759 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 if idp_window_ref.closed().unwrap_or(true)
769 && !login_complete_clone.load(Ordering::SeqCst)
770 {
771 let _ = idp_window_ref.close(); EVENT_HANDLERS.with(|cell| {
776 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 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 if !login_complete_clone.load(Ordering::SeqCst) {
798 if let Some(on_error) = on_error {
800 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 login_complete_clone.store(true, Ordering::SeqCst);
813 }
814 }
815 } else {
816 }
819 }
820 }
821 });
822 }
823
824 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 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 let login_complete = client.login_complete.clone();
853 let on_error = options.on_error.clone();
854 let client_id = *client.id; spawn_local(async move {
857 login_complete.store(true, Ordering::SeqCst);
859
860 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 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 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 #[cfg(feature = "tracing")]
979 error!("Error during handle_success: {}", e);
980 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 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 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 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 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 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#[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 fn new() -> Self {
1093 Self::default()
1094 }
1095
1096 pub fn identity(mut self, identity: ArcIdentity) -> Self {
1098 self.identity = Some(identity);
1099 self
1100 }
1101
1102 pub fn storage(mut self, storage: AuthClientStorageType) -> Self {
1104 self.storage = Some(storage);
1105 self
1106 }
1107
1108 pub fn key_type(mut self, key_type: BaseKeyType) -> Self {
1110 self.key_type = Some(key_type);
1111 self
1112 }
1113
1114 pub fn idle_options(mut self, idle_options: IdleOptions) -> Self {
1116 self.idle_options = Some(idle_options);
1117 self
1118 }
1119
1120 fn idle_options_mut(&mut self) -> &mut IdleOptions {
1124 self.idle_options.get_or_insert_with(IdleOptions::default)
1125 }
1126
1127 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 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 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 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 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 if Arc::strong_count(&options.idle_manager_options.on_idle) == 0 {
1162 options.idle_manager_options.on_idle = Arc::new(Mutex::new(Vec::new()));
1164 }
1165 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 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 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 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 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#[derive(Clone, Default)]
1323pub struct AuthClientLoginOptions {
1324 identity_provider: Option<web_sys::Url>,
1326
1327 max_time_to_live: Option<u64>,
1329
1330 allow_pin_authentication: Option<bool>,
1334
1335 derivation_origin: Option<web_sys::Url>,
1339
1340 window_opener_features: Option<String>,
1347
1348 on_success: Option<OnSuccess>,
1350
1351 on_error: Option<OnError>,
1353
1354 custom_values: Option<HashMap<String, serde_json::Value>>,
1356}
1357
1358impl AuthClientLoginOptions {
1359 pub fn builder() -> AuthClientLoginOptionsBuilder {
1361 AuthClientLoginOptionsBuilder::new()
1362 }
1363}
1364
1365pub 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 pub fn identity_provider(mut self, identity_provider: web_sys::Url) -> Self {
1393 self.identity_provider = Some(identity_provider);
1394 self
1395 }
1396
1397 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 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 pub fn derivation_origin(mut self, derivation_origin: web_sys::Url) -> Self {
1415 self.derivation_origin = Some(derivation_origin);
1416 self
1417 }
1418
1419 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 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 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 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 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#[derive(Default, Clone)]
1471pub struct AuthClientCreateOptions {
1472 pub identity: Option<ArcIdentity>,
1474 pub storage: Option<AuthClientStorageType>,
1476 pub key_type: Option<BaseKeyType>,
1478 pub idle_options: Option<IdleOptions>,
1480}
1481
1482#[derive(Default, Clone, Debug)]
1484pub struct IdleOptions {
1485 pub disable_idle: Option<bool>,
1487 pub disable_default_idle_callback: Option<bool>,
1489 pub idle_manager_options: IdleManagerOptions,
1491}
1492
1493impl IdleOptions {
1494 pub fn builder() -> IdleOptionsBuilder {
1496 IdleOptionsBuilder::new()
1497 }
1498}
1499
1500pub 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 pub fn disable_idle(mut self, disable_idle: bool) -> Self {
1518 self.disable_idle = Some(disable_idle);
1519 self
1520 }
1521
1522 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 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 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 pub fn add_on_idle<F>(mut self, on_idle: F) -> Self
1545 where
1546 F: FnMut() + Send + 'static,
1547 {
1548 if Arc::strong_count(&self.idle_manager_options.on_idle) == 0 {
1550 self.idle_manager_options.on_idle = Arc::new(Mutex::new(Vec::new()));
1552 }
1553 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 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 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
1595pub enum BaseKeyType {
1596 #[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 ); }
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}