1#[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;
66pub const ERROR_USER_INTERRUPT: &str = "UserInterrupt";
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
74#[serde(rename_all = "camelCase")]
75struct InternetIdentityAuthRequest {
76 pub kind: String,
78 pub session_public_key: Vec<u8>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub max_time_to_live: Option<u64>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub allow_pin_authentication: Option<bool>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub derivation_origin: Option<String>,
89}
90
91#[derive(Debug, Clone)]
96pub struct AuthResponseSuccess {
97 pub delegations: Vec<SignedDelegation>,
99 pub user_public_key: Vec<u8>,
101 pub authn_method: String,
103}
104
105#[derive(Debug, Clone, Deserialize)]
111#[serde(rename_all = "camelCase")]
112struct IdentityServiceResponseMessage {
113 kind: String,
115
116 delegations: Option<Vec<SignedDelegation>>,
118
119 user_public_key: Option<Vec<u8>>,
121
122 authn_method: Option<String>,
124
125 text: Option<String>,
127}
128
129impl IdentityServiceResponseMessage {
130 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#[derive(Debug, Clone)]
151enum IdentityServiceResponseKind {
152 Ready,
154 AuthSuccess(AuthResponseSuccess),
156 AuthFailure(String),
158}
159
160#[derive(Clone, Debug)]
163pub struct AuthClient {
164 identity: Arc<Mutex<ArcIdentity>>,
166 key: Key,
168 storage: AuthClientStorageType,
170 chain: Arc<Mutex<Option<DelegationChain>>>,
172 pub idle_manager: Option<IdleManager>,
174 idle_options: Option<IdleOptions>,
176 id: Arc<usize>,
179 login_complete: Arc<AtomicBool>,
181}
182
183impl Drop for AuthClient {
184 fn drop(&mut self) {
185 if Arc::strong_count(&self.id) == 1 {
187 let id_val = *self.id; 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 if let Some(window) = map.remove(&id_val) {
207 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 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 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 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 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 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 const DEFAULT_TIME_TO_LIVE: u64 = 8 * 60 * 60 * 1_000_000_000;
264
265 pub fn builder() -> AuthClientBuilder {
267 AuthClientBuilder::new()
268 }
269
270 pub async fn new() -> Result<Self, DelegationError> {
272 Self::new_with_options(AuthClientCreateOptions::default()).await
273 }
274
275 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 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 let chain_result = DelegationChain::from_json(&chain_storage);
318 chain = Arc::new(Mutex::new(Some(chain_result)));
319
320 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 let public_key = chain_inner.public_key.clone();
327 let delegations = chain_inner.delegations.clone();
328 Some((public_key, delegations))
329 } else {
330 None
332 }
333 } else {
334 Some((Vec::new(), Vec::new()))
336 }
337 } else {
338 Some((Vec::new(), Vec::new()))
340 }
341 };
342
343 match delegation_data {
345 Some((public_key, delegations)) => {
346 if !public_key.is_empty() {
347 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 #[cfg(feature = "tracing")]
360 info!(
361 "Found invalid delegation chain in storage - clearing credentials"
362 );
363 Self::delete_storage(&mut storage).await;
364
365 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 let id = {
391 static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
392 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 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 async fn handle_success(
455 &mut self,
456 message: AuthResponseSuccess,
457 on_success: Option<OnSuccess>,
458 ) -> Result<(), DelegationError> {
459 self.login_complete.store(true, Ordering::SeqCst);
462
463 if let Some(w) = self.take_idp_window() {
465 let _ = w.close();
467 };
468 self.take_event_handler(); let delegations = message.delegations.clone();
471 let user_public_key = message.user_public_key.clone();
472
473 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 let chain_json = delegation_chain.to_json();
488
489 let _ = self
492 .storage
493 .set(KEY_STORAGE_DELEGATION, chain_json.clone())
494 .await;
495
496 {
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 let is_auth = self.is_authenticated();
519 if !is_auth {
520 #[cfg(feature = "tracing")]
523 warn!("CRITICAL: is_authenticated() returned false after successful login");
524
525 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 if let Ok(mut guard) = self.chain.lock() {
547 *guard = Some(DelegationChain::from_json(&chain_json));
548 }
549
550 let is_auth_retry = self.is_authenticated();
552 #[cfg(feature = "tracing")]
553 debug!("After fix attempt: is_authenticated() = {}", is_auth_retry);
554
555 if !is_auth_retry {
558 if let Ok(_principal) = self.identity().sender() {
559 #[cfg(feature = "tracing")]
560 debug!("Current principal: {}", _principal);
561 }
562
563 {
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 let _final_auth_check = self.is_authenticated();
580 #[cfg(feature = "tracing")]
581 debug!("Final check: is_authenticated() = {}", _final_auth_check);
582 }
583 }
584
585 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 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 if let Some(on_success_cb) = on_success {
614 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 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 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 pub fn login(&mut self) {
665 self.login_with_options(AuthClientLoginOptions::default());
666 }
667
668 pub fn login_with_options(&mut self, options: AuthClientLoginOptions) {
670 self.login_complete.store(false, Ordering::SeqCst);
672
673 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 identity_provider_url.set_hash(IDENTITY_PROVIDER_ENDPOINT);
684
685 if let Some(idp_window) = self.take_idp_window() {
688 let _ = idp_window.close();
690 }
691 self.take_event_handler();
692
693 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 let handler =
706 self.get_event_handler(identity_provider_url.clone(), options.clone());
707 self.set_event_handler(handler);
708
709 self.check_interruption(options.on_error.clone(), self.login_complete.clone());
711 }
712 Ok(None) => {
713 self.login_complete.store(true, Ordering::SeqCst); 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 self.take_event_handler();
730 self.take_idp_window();
731 }
732 Err(e) => {
733 self.login_complete.store(true, Ordering::SeqCst); 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 self.take_event_handler();
749 self.take_idp_window();
750 }
751 }
752 }
753
754 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 sleep(1000).await;
765
766 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 if idp_window_ref.closed().unwrap_or(true)
776 && !login_complete_clone.load(Ordering::SeqCst)
777 {
778 let _ = idp_window_ref.close(); EVENT_HANDLERS.with(|cell| {
783 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 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 if !login_complete_clone.load(Ordering::SeqCst) {
805 if let Some(on_error) = on_error {
807 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 login_complete_clone.store(true, Ordering::SeqCst);
820 }
821 }
822 } else {
823 }
826 }
827 }
828 });
829 }
830
831 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 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 let login_complete = client.login_complete.clone();
860 let on_error = options.on_error.clone();
861 let client_id = *client.id; spawn_local(async move {
864 login_complete.store(true, Ordering::SeqCst);
866
867 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 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 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 #[cfg(feature = "tracing")]
986 error!("Error during handle_success: {}", e);
987 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 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 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 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 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 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#[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 fn new() -> Self {
1099 Self::default()
1100 }
1101
1102 pub fn identity(mut self, identity: ArcIdentity) -> Self {
1104 self.identity = Some(identity);
1105 self
1106 }
1107
1108 pub fn storage(mut self, storage: AuthClientStorageType) -> Self {
1110 self.storage = Some(storage);
1111 self
1112 }
1113
1114 pub fn key_type(mut self, key_type: BaseKeyType) -> Self {
1116 self.key_type = Some(key_type);
1117 self
1118 }
1119
1120 pub fn idle_options(mut self, idle_options: IdleOptions) -> Self {
1122 self.idle_options = Some(idle_options);
1123 self
1124 }
1125
1126 fn idle_options_mut(&mut self) -> &mut IdleOptions {
1130 self.idle_options.get_or_insert_with(IdleOptions::default)
1131 }
1132
1133 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 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 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 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 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 if Arc::strong_count(&options.idle_manager_options.on_idle) == 0 {
1168 options.idle_manager_options.on_idle = Arc::new(Mutex::new(Vec::new()));
1170 }
1171 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 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 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 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 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#[derive(Clone, Default)]
1329pub struct AuthClientLoginOptions {
1330 identity_provider: Option<web_sys::Url>,
1332
1333 max_time_to_live: Option<u64>,
1335
1336 allow_pin_authentication: Option<bool>,
1340
1341 derivation_origin: Option<web_sys::Url>,
1345
1346 window_opener_features: Option<String>,
1353
1354 on_success: Option<OnSuccess>,
1356
1357 on_error: Option<OnError>,
1359
1360 custom_values: Option<HashMap<String, serde_json::Value>>,
1362}
1363
1364impl AuthClientLoginOptions {
1365 pub fn builder() -> AuthClientLoginOptionsBuilder {
1367 AuthClientLoginOptionsBuilder::new()
1368 }
1369}
1370
1371pub 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 pub fn identity_provider(mut self, identity_provider: web_sys::Url) -> Self {
1399 self.identity_provider = Some(identity_provider);
1400 self
1401 }
1402
1403 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 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 pub fn derivation_origin(mut self, derivation_origin: web_sys::Url) -> Self {
1421 self.derivation_origin = Some(derivation_origin);
1422 self
1423 }
1424
1425 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 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 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 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 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#[derive(Default, Clone)]
1477pub struct AuthClientCreateOptions {
1478 pub identity: Option<ArcIdentity>,
1480 pub storage: Option<AuthClientStorageType>,
1482 pub key_type: Option<BaseKeyType>,
1484 pub idle_options: Option<IdleOptions>,
1486}
1487
1488#[derive(Default, Clone, Debug)]
1490pub struct IdleOptions {
1491 pub disable_idle: Option<bool>,
1493 pub disable_default_idle_callback: Option<bool>,
1495 pub idle_manager_options: IdleManagerOptions,
1497}
1498
1499impl IdleOptions {
1500 pub fn builder() -> IdleOptionsBuilder {
1502 IdleOptionsBuilder::new()
1503 }
1504}
1505
1506pub 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 pub fn disable_idle(mut self, disable_idle: bool) -> Self {
1524 self.disable_idle = Some(disable_idle);
1525 self
1526 }
1527
1528 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 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 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 pub fn add_on_idle<F>(mut self, on_idle: F) -> Self
1551 where
1552 F: FnMut() + Send + 'static,
1553 {
1554 if Arc::strong_count(&self.idle_manager_options.on_idle) == 0 {
1556 self.idle_manager_options.on_idle = Arc::new(Mutex::new(Vec::new()));
1558 }
1559 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 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 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
1601pub enum BaseKeyType {
1602 #[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 ); }
1705}