ic_auth_client/auth_client/
native.rs

1#[cfg(feature = "keyring")]
2use crate::storage::sync_storage::KeyringStorage;
3#[cfg(feature = "pem")]
4use crate::storage::sync_storage::PemStorage;
5use crate::{
6    ArcIdentity, AuthClientError,
7    api::AuthResponseSuccess,
8    idle_manager::{IdleManager, IdleManagerOptions},
9    key::{Key, KeyWithRaw},
10    option::{AuthClientLoginOptions, IdleOptions, native::NativeAuthClientCreateOptions},
11    storage::{
12        KEY_STORAGE_DELEGATION, KEY_STORAGE_KEY, StorageError, StoredKey,
13        sync_storage::AuthClientStorage,
14    },
15    util::{callback::OnSuccess, delegation_chain::DelegationChain},
16};
17use base64::prelude::{BASE64_STANDARD, Engine as _};
18use ed25519_dalek::SigningKey;
19use futures::{channel::oneshot, executor::block_on};
20use ic_agent::{
21    export::Principal,
22    identity::{AnonymousIdentity, DelegatedIdentity, DelegationError, Identity, SignedDelegation},
23};
24use parking_lot::Mutex;
25use serde_json::Number;
26use std::{fmt, sync::Arc, thread, time::Duration};
27#[cfg(feature = "pem")]
28use std::{
29    fs,
30    io::ErrorKind,
31    path::{Path, PathBuf},
32};
33use tiny_http::{Response, Server};
34use url::Url;
35
36/// Errors that can occur during the login process.
37///
38/// This enum represents all the possible error conditions that may arise
39/// when attempting to authenticate a user through the Internet Identity
40/// authentication flow.
41#[derive(Debug, thiserror::Error)]
42pub enum NativeLoginError {
43    /// No free ports are available on the local machine to start the callback server.
44    #[error("No free ports available")]
45    NoFreePort,
46    /// An error occurred while starting or running the local HTTP server.
47    #[error("Server error: {0}")]
48    ServerError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
49    /// Failed to parse a URL during the authentication process.
50    #[error("URL parse error: {0}")]
51    UrlParseError(#[from] url::ParseError),
52    /// Failed to open the user's default web browser for authentication.
53    #[error("Failed to open browser: {0}")]
54    BrowserOpenError(String),
55    /// The server timed out while waiting for the authentication callback.
56    #[error("Server receive timed out")]
57    ServerTimeout,
58    /// The server thread handling the authentication callback panicked.
59    #[error("Server thread panicked")]
60    ServerThreadPanicked,
61    /// Failed to receive the delegation response through the internal message channel.
62    #[error("Failed to receive delegation")]
63    OneshotRecvError,
64    /// Failed to deserialize the JSON response from the identity provider.
65    #[error("JSON deserialization error: {0}")]
66    JsonError(#[from] serde_json::Error),
67    /// An error occurred while processing the delegation chain.
68    #[error("Delegation error: {0}")]
69    DelegationError(#[from] DelegationError),
70    /// The authentication callback was missing required delegation or error parameters.
71    #[error("Missing delegation or error parameter in redirect")]
72    MissingDelegationOrError,
73    /// The callback server received a request to an unexpected URL path.
74    #[error("Unexpected request path: {0}")]
75    UnexpectedRequestPath(String),
76    /// A custom error occurred during authentication.
77    #[error("Custom error: {0}")]
78    Custom(String),
79}
80
81enum CallbackResult {
82    Success(AuthResponseSuccess),
83    Error(NativeLoginError),
84}
85
86pub(super) struct AuthClientInner {
87    pub identity: Arc<Mutex<ArcIdentity>>,
88    pub key: Key,
89    pub storage: Mutex<Box<dyn AuthClientStorage>>,
90    pub chain: Arc<Mutex<Option<DelegationChain>>>,
91    pub idle_manager: Mutex<Option<IdleManager>>,
92    pub idle_options: Option<IdleOptions>,
93}
94
95impl fmt::Debug for AuthClientInner {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.debug_struct("AuthClientInner")
98            .field("key", &self.key)
99            .field("idle_options", &self.idle_options)
100            .finish()
101    }
102}
103
104/// The tool for managing authentication and identity.
105///
106/// It maintains the state of the user's identity and provides methods for authentication.
107#[derive(Clone, Debug)]
108pub struct NativeAuthClient(Arc<AuthClientInner>);
109
110impl NativeAuthClient {
111    /// Creates a new [`AuthClient`] with default options.
112    #[cfg(feature = "keyring")]
113    pub fn new<T: AsRef<str>>(service_name: T) -> Result<Self, AuthClientError> {
114        let options = NativeAuthClientCreateOptions::builder()
115            .storage(KeyringStorage::new(service_name.as_ref()))
116            .build();
117        Self::new_with_options(options)
118    }
119
120    /// Creates a new [`AuthClient`] using the first PEM file found inside the provided directory.
121    #[cfg(feature = "pem")]
122    pub fn new_with_pem_directory<T, P>(
123        service_name: T,
124        directory: P,
125    ) -> Result<Self, AuthClientError>
126    where
127        T: Into<String>,
128        P: Into<PathBuf>,
129    {
130        let service_name = service_name.into();
131        let directory = directory.into();
132
133        let mut storage_dir = directory.clone();
134        storage_dir.push(format!(
135            "ic-auth-client-{}",
136            sanitize_service_name(&service_name)
137        ));
138
139        let mut storage = PemStorage::new(storage_dir);
140
141        let key_exists = storage.get(KEY_STORAGE_KEY)?.is_some();
142
143        if !key_exists {
144            if let Some(pem_path) = find_pem_file_in_directory(&directory)? {
145                storage.import_private_key_from_pem_file(pem_path)?;
146            }
147        }
148
149        let options = NativeAuthClientCreateOptions::builder()
150            .storage(storage)
151            .build();
152        Self::new_with_options(options)
153    }
154
155    /// Creates a new key if one is not found in storage, otherwise loads the existing key.
156    fn create_or_load_key(
157        identity: Option<ArcIdentity>,
158        storage: &mut dyn AuthClientStorage,
159    ) -> Result<Key, AuthClientError> {
160        match identity {
161            Some(identity) => Ok(Key::Identity(identity)),
162            None => match storage.get(KEY_STORAGE_KEY) {
163                Ok(Some(stored_key)) => {
164                    let private_key = stored_key.decode().map_err(|e| {
165                        DelegationError::IdentityError(format!(
166                            "Failed to decode private key: {}",
167                            e
168                        ))
169                    })?;
170                    Ok(Key::WithRaw(KeyWithRaw::new(private_key)))
171                }
172                Ok(None) => {
173                    let mut rng = rand::thread_rng();
174                    let private_key = SigningKey::generate(&mut rng).to_bytes();
175                    storage.set(KEY_STORAGE_KEY, StoredKey::Raw(private_key))?;
176                    Ok(Key::WithRaw(KeyWithRaw::new(private_key)))
177                }
178                Err(e) => Err(e.into()),
179            },
180        }
181    }
182
183    /// Extracts delegation data from a delegation chain if it is valid.
184    fn get_delegation_data(
185        chain: &Option<DelegationChain>,
186    ) -> Option<(Vec<u8>, Vec<SignedDelegation>)> {
187        if let Some(chain_inner) = chain.as_ref() {
188            if chain_inner.is_delegation_valid(None) {
189                let public_key = chain_inner.public_key.clone();
190                let delegations = chain_inner.delegations.clone();
191                Some((public_key, delegations))
192            } else {
193                None
194            }
195        } else {
196            Some((Vec::new(), Vec::new()))
197        }
198    }
199
200    /// Loads a delegation chain from storage and updates the identity if the chain is valid.
201    fn load_delegation_chain(
202        storage: &mut dyn AuthClientStorage,
203        key: &Key,
204    ) -> (Option<DelegationChain>, ArcIdentity) {
205        let mut identity = ArcIdentity::from(key.clone());
206        let mut chain: Option<DelegationChain> = None;
207
208        match storage.get(KEY_STORAGE_DELEGATION) {
209            Ok(Some(chain_stored)) => {
210                let chain_stored = chain_stored.encode();
211                let chain_result = DelegationChain::from_json(&chain_stored);
212                chain = Some(chain_result);
213
214                let delegation_data = Self::get_delegation_data(&chain);
215
216                match delegation_data {
217                    Some((public_key, delegations)) => {
218                        if !public_key.is_empty() {
219                            identity =
220                                ArcIdentity::Delegated(Arc::new(DelegatedIdentity::new_unchecked(
221                                    public_key,
222                                    Box::new(key.as_arc_identity()),
223                                    delegations,
224                                )));
225                        }
226                    }
227                    None => {
228                        #[cfg(feature = "tracing")]
229                        info!("Found invalid delegation chain in storage - clearing credentials");
230                        let _ = Self::delete_storage_native(storage);
231                        identity = ArcIdentity::Anonymous(Arc::new(AnonymousIdentity));
232                        chain = None;
233                    }
234                }
235            }
236            Ok(None) => (),
237            Err(_e) => {
238                #[cfg(feature = "tracing")]
239                error!("Failed to load delegation chain from storage: {}", _e);
240            }
241        }
242        (chain, identity)
243    }
244
245    /// Creates an idle manager if idle detection is not disabled.
246    fn create_idle_manager(
247        idle_options: &Option<IdleOptions>,
248        chain: &Option<DelegationChain>,
249        identity_is_some: bool,
250    ) -> Option<IdleManager> {
251        if !idle_options
252            .as_ref()
253            .and_then(|o| o.disable_idle)
254            .unwrap_or(false)
255            && (chain.is_some() || identity_is_some)
256        {
257            let idle_manager_options: Option<IdleManagerOptions> = idle_options
258                .as_ref()
259                .map(|o| o.idle_manager_options.clone());
260            Some(IdleManager::new(idle_manager_options))
261        } else {
262            None
263        }
264    }
265
266    /// Creates a new [`AuthClient`] with the provided options.
267    pub fn new_with_options(
268        options: NativeAuthClientCreateOptions,
269    ) -> Result<Self, AuthClientError> {
270        let identity = options.identity.clone();
271        let options_identity_is_some = identity.is_some();
272        let mut storage = options.storage;
273
274        let key = Self::create_or_load_key(identity, storage.as_mut())?;
275
276        let (chain, identity) = Self::load_delegation_chain(storage.as_mut(), &key);
277
278        let idle_manager =
279            Self::create_idle_manager(&options.idle_options, &chain, options_identity_is_some);
280
281        Ok(Self(Arc::new(AuthClientInner {
282            identity: Arc::new(Mutex::new(identity)),
283            key,
284            storage: Mutex::new(storage),
285            chain: Arc::new(Mutex::new(chain)),
286            idle_manager: Mutex::new(idle_manager),
287            idle_options: options.idle_options,
288        })))
289    }
290
291    /// Registers the default idle callback, which logs the user out on idle.
292    fn register_default_idle_callback_native(&self) {
293        if let Some(options) = self.0.idle_options.as_ref() {
294            if options.disable_default_idle_callback.unwrap_or_default() {
295                return;
296            }
297
298            if options.idle_manager_options.on_idle.lock().is_empty() {
299                if let Some(idle_manager) = self.0.idle_manager.lock().as_ref() {
300                    let client = self.clone();
301                    let callback = move || {
302                        client.logout();
303                    };
304                    idle_manager.register_callback(callback);
305                }
306            }
307        }
308    }
309
310    /// Returns the identity of the user.
311    pub fn identity(&self) -> Arc<dyn Identity> {
312        self.0.identity.lock().as_arc_identity()
313    }
314
315    /// Returns the principal of the user.
316    pub fn principal(&self) -> Result<Principal, String> {
317        self.identity().sender()
318    }
319
320    /// Checks if the user is authenticated.
321    pub fn is_authenticated(&self) -> bool {
322        let is_not_anonymous = self
323            .identity()
324            .sender()
325            .map(|s| s != Principal::anonymous())
326            .unwrap_or(false);
327
328        let is_valid_chain = self
329            .0
330            .chain
331            .lock()
332            .as_ref()
333            .is_some_and(|c| c.is_delegation_valid(None));
334
335        is_not_anonymous && is_valid_chain
336    }
337
338    /// Returns the idle manager if it exists.
339    pub fn idle_manager(&self) -> Option<IdleManager> {
340        self.0.idle_manager.lock().clone()
341    }
342
343    /// Handles a successful authentication response.
344    fn handle_success(
345        &self,
346        message: AuthResponseSuccess,
347        on_success: Option<OnSuccess>,
348    ) -> Result<(), DelegationError> {
349        let delegations = message.delegations.clone();
350        let user_public_key = message.user_public_key.clone();
351
352        let delegation_chain = DelegationChain {
353            delegations: delegations.clone(),
354            public_key: user_public_key.clone(),
355        };
356
357        self.update_storage_with_delegation(&delegation_chain);
358        self.update_identity_with_delegation(
359            &delegation_chain,
360            user_public_key.clone(),
361            delegations.clone(),
362        );
363
364        self.verify_and_fix_authentication(
365            &user_public_key,
366            &delegations,
367            &delegation_chain.to_json(),
368        );
369
370        self.maybe_create_idle_manager();
371
372        if let Some(on_success_cb) = on_success {
373            on_success_cb.0.lock()(message.clone());
374        }
375
376        Ok(())
377    }
378
379    /// Stores the delegation chain and key in storage.
380    fn update_storage_with_delegation(&self, delegation_chain: &DelegationChain) {
381        if let Key::WithRaw(key) = &self.0.key {
382            if let Err(_e) = self
383                .0
384                .storage
385                .lock()
386                .set(KEY_STORAGE_KEY, StoredKey::Raw(*key.raw_key()))
387            {
388                #[cfg(feature = "tracing")]
389                error!("Failed to store key: {}", _e);
390            }
391        }
392
393        let chain_json = delegation_chain.to_json();
394        if let Err(_e) = self.0.storage.lock().set(
395            KEY_STORAGE_DELEGATION,
396            StoredKey::String(chain_json.clone()),
397        ) {
398            #[cfg(feature = "tracing")]
399            error!("Failed to store delegation: {}", _e);
400        }
401    }
402
403    /// Updates the in-memory identity with the new delegation.
404    fn update_identity_with_delegation(
405        &self,
406        delegation_chain: &DelegationChain,
407        user_public_key: Vec<u8>,
408        delegations: Vec<SignedDelegation>,
409    ) {
410        *self.0.chain.lock() = Some(delegation_chain.clone());
411        *self.0.identity.lock() =
412            ArcIdentity::Delegated(Arc::new(DelegatedIdentity::new_unchecked(
413                user_public_key,
414                Box::new(self.0.key.as_arc_identity()),
415                delegations,
416            )));
417    }
418
419    /// Verifies that the user is authenticated and attempts to fix the state if not.
420    fn verify_and_fix_authentication(
421        &self,
422        user_public_key: &[u8],
423        delegations: &[SignedDelegation],
424        chain_json: &str,
425    ) {
426        if self.is_authenticated() {
427            return;
428        }
429
430        #[cfg(feature = "tracing")]
431        warn!("CRITICAL: is_authenticated() returned false after successful login");
432
433        let _is_not_anonymous = self
434            .identity()
435            .sender()
436            .map(|s| s != Principal::anonymous())
437            .unwrap_or(false);
438        let _has_chain = self.0.chain.lock().is_some();
439        #[cfg(feature = "tracing")]
440        debug!(
441            "is_authenticated(): is_not_anonymous={}, has_chain={}",
442            _is_not_anonymous, _has_chain
443        );
444
445        *self.0.chain.lock() = Some(DelegationChain::from_json(chain_json));
446
447        let is_auth_retry = self.is_authenticated();
448        #[cfg(feature = "tracing")]
449        debug!("After fix attempt: is_authenticated() = {}", is_auth_retry);
450
451        if !is_auth_retry {
452            if let Ok(_principal) = self.identity().sender() {
453                #[cfg(feature = "tracing")]
454                debug!("Current principal: {}", _principal);
455            }
456
457            *self.0.identity.lock() =
458                ArcIdentity::Delegated(Arc::new(DelegatedIdentity::new_unchecked(
459                    user_public_key.to_vec(),
460                    Box::new(self.0.key.as_arc_identity()),
461                    delegations.to_vec(),
462                )));
463
464            let _final_auth_check = self.is_authenticated();
465            #[cfg(feature = "tracing")]
466            debug!("Final check: is_authenticated() = {}", _final_auth_check);
467        }
468    }
469
470    /// Creates an idle manager if one does not exist and idle detection is not disabled.
471    fn maybe_create_idle_manager(&self) {
472        let disable_idle = self
473            .0
474            .idle_options
475            .as_ref()
476            .and_then(|o| o.disable_idle)
477            .unwrap_or(false);
478
479        if self.0.idle_manager.lock().is_none() && !disable_idle {
480            let idle_manager_options = self
481                .0
482                .idle_options
483                .as_ref()
484                .map(|o| o.idle_manager_options.clone());
485            let new_idle_manager = IdleManager::new(idle_manager_options);
486            *self.0.idle_manager.lock() = Some(new_idle_manager);
487
488            if self.0.idle_manager.lock().is_some() {
489                self.register_default_idle_callback_native();
490            }
491        }
492    }
493
494    /// Handles the redirect from the identity provider.
495    fn handle_get_redirect(request: tiny_http::Request, tx: oneshot::Sender<CallbackResult>) {
496        let url = match Url::parse(&format!("http://localhost{}", request.url())) {
497            Ok(url) => url,
498            Err(e) => {
499                Self::respond_with_html_error(
500                    request,
501                    tx,
502                    NativeLoginError::UrlParseError(e),
503                    "Login window closed or redirect failed.",
504                );
505                return;
506            }
507        };
508
509        let payload = url
510            .query_pairs()
511            .find(|(key, _)| key == "payload")
512            .map(|(_, value)| value.into_owned());
513
514        let Some(payload_value) = payload else {
515            Self::respond_with_html_error(
516                request,
517                tx,
518                NativeLoginError::MissingDelegationOrError,
519                "Missing authentication payload.",
520            );
521            return;
522        };
523
524        let mut json = match Self::deserialize_payload(&payload_value) {
525            Ok(json) => json,
526            Err(err) => {
527                let message = match err {
528                    NativeLoginError::JsonError(_) => "Failed to parse authentication payload.",
529                    _ => "Invalid authentication payload.",
530                };
531                Self::respond_with_html_error(request, tx, err, message);
532                return;
533            }
534        };
535
536        if let Err(err) = Self::normalize_delegations(&mut json) {
537            Self::respond_with_html_error(request, tx, err, "Invalid authentication payload.");
538            return;
539        }
540
541        Self::respond_with_callback(request, tx, Self::process_auth_payload(json, true));
542    }
543
544    /// Handles incoming POST requests with authentication data.
545    fn handle_post_callback(request: tiny_http::Request, tx: oneshot::Sender<CallbackResult>) {
546        let mut request = request;
547        let mut content = String::new();
548        if let Err(e) = request.as_reader().read_to_string(&mut content) {
549            let _ = tx.send(CallbackResult::Error(NativeLoginError::ServerError(
550                Box::new(e),
551            )));
552            let _ = request.respond(Self::cors_response("Error reading request body", 500));
553            return;
554        }
555
556        let mut json: serde_json::Value = match serde_json::from_str(&content) {
557            Ok(json) => json,
558            Err(e) => {
559                let _ = tx.send(CallbackResult::Error(NativeLoginError::JsonError(e)));
560                let _ = request.respond(Self::cors_response("Error parsing JSON body", 400));
561                return;
562            }
563        };
564
565        if let Err(err) = Self::normalize_delegations(&mut json) {
566            let _ = tx.send(CallbackResult::Error(err));
567            let _ = request.respond(Self::cors_response("Invalid authentication payload", 400));
568            return;
569        }
570
571        Self::respond_with_callback(request, tx, Self::process_auth_payload(json, false));
572    }
573
574    fn process_auth_payload(
575        json: serde_json::Value,
576        render_html: bool,
577    ) -> (Response<std::io::Cursor<Vec<u8>>>, CallbackResult) {
578        let response_type = json["type"].as_str();
579
580        match response_type {
581            Some("success") => {
582                match serde_json::from_value::<AuthResponseSuccess>(json["data"].clone()) {
583                    Ok(success_data) => {
584                        let response = if render_html {
585                            Self::html_response(
586                                "<h1>Login successful</h1><p>You can close this window.</p>",
587                                200,
588                            )
589                        } else {
590                            Self::cors_response("OK", 200)
591                        };
592                        (response, CallbackResult::Success(success_data))
593                    }
594                    Err(e) => {
595                        let response = if render_html {
596                            Self::html_response(
597                                "<h1>Login failed</h1><p>Invalid success payload.</p>",
598                                400,
599                            )
600                        } else {
601                            Self::cors_response("Error parsing success data", 400)
602                        };
603                        (
604                            response,
605                            CallbackResult::Error(NativeLoginError::JsonError(e)),
606                        )
607                    }
608                }
609            }
610            Some("error") => {
611                let error_message = json["data"].as_str().unwrap_or("Unknown error").to_string();
612                let response = if render_html {
613                    Self::html_response(
614                        &format!("<h1>Login failed</h1><p>{}</p>", error_message),
615                        400,
616                    )
617                } else {
618                    Self::cors_response("Error", 200)
619                };
620                (
621                    response,
622                    CallbackResult::Error(NativeLoginError::Custom(error_message)),
623                )
624            }
625            _ => {
626                let response = if render_html {
627                    Self::html_response("<h1>Login failed</h1><p>Invalid response type.</p>", 400)
628                } else {
629                    Self::cors_response("Invalid response type", 400)
630                };
631                (
632                    response,
633                    CallbackResult::Error(NativeLoginError::Custom(
634                        "Invalid response type".to_string(),
635                    )),
636                )
637            }
638        }
639    }
640
641    fn respond_with_html_error(
642        request: tiny_http::Request,
643        tx: oneshot::Sender<CallbackResult>,
644        error: NativeLoginError,
645        message: &str,
646    ) {
647        let _ = tx.send(CallbackResult::Error(error));
648        let body = format!("<h1>Login failed</h1><p>{}</p>", message);
649        let _ = request.respond(Self::html_response(&body, 400));
650    }
651
652    fn respond_with_callback(
653        request: tiny_http::Request,
654        tx: oneshot::Sender<CallbackResult>,
655        outcome: (Response<std::io::Cursor<Vec<u8>>>, CallbackResult),
656    ) {
657        let (response, callback_result) = outcome;
658        let _ = tx.send(callback_result);
659        let _ = request.respond(response);
660    }
661
662    fn html_response(body: &str, status_code: u16) -> Response<std::io::Cursor<Vec<u8>>> {
663        Response::from_string(body)
664            .with_status_code(status_code)
665            .with_header(
666                tiny_http::Header::from_bytes(b"Content-Type", b"text/html; charset=utf-8")
667                    .unwrap(),
668            )
669    }
670
671    fn cors_response(body: &str, status_code: u16) -> Response<std::io::Cursor<Vec<u8>>> {
672        let mut response = Response::from_string(body).with_status_code(status_code);
673        response = response.with_header(
674            tiny_http::Header::from_bytes(b"Access-Control-Allow-Origin", b"*").unwrap(),
675        );
676        response = response.with_header(
677            tiny_http::Header::from_bytes(b"Access-Control-Allow-Headers", b"Content-Type")
678                .unwrap(),
679        );
680        response = response.with_header(
681            tiny_http::Header::from_bytes(b"Access-Control-Allow-Methods", b"POST, OPTIONS")
682                .unwrap(),
683        );
684        response.with_header(
685            tiny_http::Header::from_bytes(b"Access-Control-Allow-Private-Network", b"true")
686                .unwrap(),
687        )
688    }
689
690    fn deserialize_payload(payload: &str) -> Result<serde_json::Value, NativeLoginError> {
691        let decoded_payload = BASE64_STANDARD
692            .decode(payload.as_bytes())
693            .map_err(|e| NativeLoginError::Custom(format!("Invalid payload encoding: {}", e)))?;
694        let mut json = serde_json::from_slice(&decoded_payload)?;
695        Self::normalize_delegations(&mut json)?;
696        Ok(json)
697    }
698
699    fn normalize_delegations(json: &mut serde_json::Value) -> Result<(), NativeLoginError> {
700        let Some(data) = json.get_mut("data") else {
701            return Ok(());
702        };
703
704        let Some(delegations) = data.get_mut("delegations").and_then(|d| d.as_array_mut()) else {
705            return Ok(());
706        };
707
708        for delegation in delegations.iter_mut() {
709            let Some(expiration_value) = delegation
710                .get_mut("delegation")
711                .and_then(|d| d.get_mut("expiration"))
712            else {
713                continue;
714            };
715
716            if let Some(exp_str) = expiration_value.as_str() {
717                let parsed = exp_str.parse::<u64>().map_err(|e| {
718                    NativeLoginError::Custom(format!("Invalid delegation expiration: {}", e))
719                })?;
720                *expiration_value = serde_json::Value::Number(Number::from(parsed));
721            }
722        }
723
724        Ok(())
725    }
726
727    /// Finishes the login process after the delegation has been received.
728    async fn finish_login(
729        &self,
730        rx: oneshot::Receiver<CallbackResult>,
731        on_success: Option<OnSuccess>,
732    ) -> Result<(), NativeLoginError> {
733        let callback_result = rx.await.map_err(|_| NativeLoginError::OneshotRecvError)?;
734
735        match callback_result {
736            CallbackResult::Success(auth_success) => {
737                match self.handle_success(auth_success, on_success) {
738                    Ok(_) => Ok(()),
739                    Err(e) => Err(NativeLoginError::DelegationError(e)),
740                }
741            }
742            CallbackResult::Error(e) => Err(e),
743        }
744    }
745
746    /// Logs the user in by opening a browser window to the identity provider.
747    pub fn login<T: AsRef<str> + Send + 'static>(
748        &self,
749        ii_url: T,
750        options: AuthClientLoginOptions,
751    ) {
752        let client = self.clone();
753        let ii_url = ii_url.as_ref().to_string();
754
755        thread::spawn(move || {
756            let on_error = options.on_error.clone();
757            if let Err(e) = block_on(client.login_task(ii_url, options)) {
758                if let Some(on_error) = on_error {
759                    on_error.0.lock()(Some(e.to_string()));
760                }
761            }
762        });
763    }
764
765    fn start_http_server(server: Server, tx: oneshot::Sender<CallbackResult>, timeout: Duration) {
766        thread::spawn(move || {
767            let start_time = std::time::Instant::now();
768
769            while start_time.elapsed() < timeout {
770                let request = match server.recv_timeout(Duration::from_millis(500)) {
771                    Ok(Some(request)) => request,
772                    Ok(None) => continue,
773                    Err(e) => {
774                        #[cfg(feature = "tracing")]
775                        error!("Server error while receiving request: {}", e);
776                        let _ = tx.send(CallbackResult::Error(NativeLoginError::ServerError(
777                            Box::new(e),
778                        )));
779                        return;
780                    }
781                };
782
783                if request.method() == &tiny_http::Method::Options
784                    && request.url().starts_with("/auth-callback")
785                {
786                    let response = Self::cors_response("", 204);
787                    if let Err(_e) = request.respond(response) {
788                        #[cfg(feature = "tracing")]
789                        error!("Failed to respond to OPTIONS request: {}", _e);
790                    }
791                    continue;
792                }
793
794                let handler: Option<fn(tiny_http::Request, oneshot::Sender<CallbackResult>)> =
795                    if request.method() == &tiny_http::Method::Post
796                        && request.url().starts_with("/auth-callback")
797                    {
798                        Some(Self::handle_post_callback)
799                    } else if request.method() == &tiny_http::Method::Get
800                        && request.url().starts_with("/auth-callback")
801                    {
802                        Some(Self::handle_get_redirect)
803                    } else {
804                        None
805                    };
806
807                if let Some(handler_fn) = handler {
808                    handler_fn(request, tx);
809                    return;
810                }
811
812                // Fallback for any other request.
813                let response = Response::from_string("").with_status_code(204);
814                if let Err(_e) = request.respond(response) {
815                    #[cfg(feature = "tracing")]
816                    error!("Failed to respond to unexpected request: {}", _e);
817                }
818            }
819
820            let _ = tx.send(CallbackResult::Error(NativeLoginError::ServerTimeout));
821        });
822    }
823
824    async fn login_task<T: AsRef<str>>(
825        &self,
826        ii_url: T,
827        options: AuthClientLoginOptions,
828    ) -> Result<(), NativeLoginError> {
829        let port = portpicker::pick_unused_port().ok_or(NativeLoginError::NoFreePort)?;
830        let redirect_uri = format!("http://127.0.0.1:{}/auth-callback", port);
831
832        let server = Server::http(format!("127.0.0.1:{}", port))?;
833        let (tx, rx) = oneshot::channel::<CallbackResult>();
834
835        let public_key_hex = hex::encode(self.0.key.public_key().unwrap());
836
837        let mut url = Url::parse(ii_url.as_ref()).map_err(NativeLoginError::UrlParseError)?;
838        Self::set_query_params(&mut url, &options, &redirect_uri, &public_key_hex);
839
840        webbrowser::open(url.as_str())
841            .map_err(|e| NativeLoginError::BrowserOpenError(e.to_string()))?;
842
843        let timeout = options.timeout.unwrap_or(Duration::from_secs(300));
844        Self::start_http_server(server, tx, timeout);
845
846        self.finish_login(rx, options.on_success).await
847    }
848
849    /// Sets the query parameters for the identity provider URL.
850    fn set_query_params(
851        url: &mut Url,
852        options: &AuthClientLoginOptions,
853        redirect_uri: &str,
854        public_key_hex: &str,
855    ) {
856        let mut query_pairs = url.query_pairs_mut();
857        query_pairs
858            .append_pair("redirectUri", redirect_uri)
859            .append_pair("pubkey", public_key_hex);
860
861        if let Some(ref identity_provider) = options.identity_provider {
862            query_pairs.append_pair("identityProvider", identity_provider);
863        }
864
865        if let Some(ref max_time_to_live) = options.max_time_to_live {
866            query_pairs.append_pair("maxTimeToLive", &max_time_to_live.to_string());
867        }
868
869        if let Some(ref allow_pin_authentication) = options.allow_pin_authentication {
870            query_pairs.append_pair(
871                "allowPinAuthentication",
872                &allow_pin_authentication.to_string(),
873            );
874        }
875
876        if let Some(ref derivation_origin) = options.derivation_origin {
877            query_pairs.append_pair("derivationOrigin", derivation_origin);
878        }
879
880        if let Some(ref window_opener_features) = options.window_opener_features {
881            query_pairs.append_pair("windowOpenerFeatures", window_opener_features);
882        }
883
884        if let Some(ref custom_values) = options.custom_values {
885            if let Ok(json) = serde_json::to_string(custom_values) {
886                query_pairs.append_pair("customValues", &json);
887            }
888        }
889    }
890
891    /// Core logout logic that clears identity and storage.
892    fn logout_core(
893        identity: Arc<Mutex<ArcIdentity>>,
894        storage: &mut dyn AuthClientStorage,
895        chain: Arc<Mutex<Option<DelegationChain>>>,
896    ) {
897        if let Err(_e) = Self::delete_storage_native(storage) {
898            #[cfg(feature = "tracing")]
899            error!("Failed to delete storage: {}", _e);
900        }
901
902        // Reset this auth client to a non-authenticated state.
903        *identity.lock() = ArcIdentity::Anonymous(Arc::new(AnonymousIdentity));
904        chain.lock().take();
905    }
906
907    /// Log the user out.
908    pub fn logout(&self) {
909        if let Some(idle_manager) = self.0.idle_manager.lock().take() {
910            drop(idle_manager);
911        }
912
913        let mut storage_lock = self.0.storage.lock();
914        let storage_ref: &mut dyn AuthClientStorage = &mut **storage_lock;
915        Self::logout_core(self.0.identity.clone(), storage_ref, self.0.chain.clone());
916    }
917
918    /// Deletes the key and delegation from storage.
919    fn delete_storage_native(
920        storage: &mut dyn AuthClientStorage,
921    ) -> Result<(), crate::storage::StorageError> {
922        storage.remove(KEY_STORAGE_KEY)?;
923        storage.remove(KEY_STORAGE_DELEGATION)?;
924        Ok(())
925    }
926}
927
928#[cfg(feature = "pem")]
929fn sanitize_service_name(name: &str) -> String {
930    name.chars()
931        .map(|c| {
932            if matches!(c, '/' | '\\' | ':' | '*') {
933                '_'
934            } else {
935                c
936            }
937        })
938        .collect()
939}
940
941#[cfg(feature = "pem")]
942fn find_pem_file_in_directory(directory: &Path) -> Result<Option<PathBuf>, AuthClientError> {
943    let entries = match fs::read_dir(directory) {
944        Ok(entries) => entries,
945        Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None),
946        Err(e) => return Err(AuthClientError::Storage(StorageError::File(e.to_string()))),
947    };
948
949    for entry in entries {
950        let entry =
951            entry.map_err(|e| AuthClientError::Storage(StorageError::File(e.to_string())))?;
952        let path = entry.path();
953        if path.is_file() {
954            if let Some(ext) = path.extension() {
955                if ext.eq_ignore_ascii_case("pem") {
956                    return Ok(Some(path));
957                }
958            }
959        }
960    }
961
962    Ok(None)
963}
964
965#[cfg(test)]
966mod tests {
967    use super::*;
968    use serde_json::json;
969
970    fn has_header(response: &Response<std::io::Cursor<Vec<u8>>>, key: &str, value: &str) -> bool {
971        response.headers().iter().any(|header| {
972            let header_name: &str = header.field.as_str().as_ref();
973            let header_value: &str = header.value.as_str();
974            header_name.eq_ignore_ascii_case(key) && header_value.eq_ignore_ascii_case(value)
975        })
976    }
977
978    #[test]
979    fn cors_response_exposes_private_network_headers() {
980        let response = NativeAuthClient::cors_response("", 204);
981        assert_eq!(response.status_code().0, 204);
982        assert!(has_header(
983            &response,
984            "Access-Control-Allow-Private-Network",
985            "true"
986        ));
987        assert!(has_header(&response, "Access-Control-Allow-Origin", "*"));
988    }
989
990    #[test]
991    fn process_auth_payload_returns_success_and_cors_headers() {
992        let payload = json!({
993            "type": "success",
994            "data": {
995                "delegations": [],
996                "userPublicKey": [],
997                "authnMethod": "native"
998            }
999        });
1000        let (response, callback) = NativeAuthClient::process_auth_payload(payload, false);
1001        assert_eq!(response.status_code().0, 200);
1002        assert!(has_header(&response, "Access-Control-Allow-Origin", "*"));
1003        assert!(has_header(
1004            &response,
1005            "Access-Control-Allow-Private-Network",
1006            "true"
1007        ));
1008
1009        match callback {
1010            CallbackResult::Success(data) => assert_eq!(data.authn_method, "native"),
1011            CallbackResult::Error(err) => panic!("unexpected error: {:?}", err),
1012        }
1013    }
1014
1015    #[test]
1016    fn process_auth_payload_renders_html_when_requested() {
1017        let payload = json!({
1018            "type": "success",
1019            "data": {
1020                "delegations": [],
1021                "userPublicKey": [],
1022                "authnMethod": "redirect"
1023            }
1024        });
1025        let (response, callback) = NativeAuthClient::process_auth_payload(payload, true);
1026        assert_eq!(response.status_code().0, 200);
1027        assert!(has_header(
1028            &response,
1029            "Content-Type",
1030            "text/html; charset=utf-8"
1031        ));
1032        assert!(matches!(callback, CallbackResult::Success(_)));
1033    }
1034
1035    #[test]
1036    fn process_auth_payload_handles_remote_errors() {
1037        let payload = json!({
1038            "type": "error",
1039            "data": "Browser closed"
1040        });
1041        let (response, callback) = NativeAuthClient::process_auth_payload(payload, false);
1042        assert_eq!(response.status_code().0, 200);
1043        match callback {
1044            CallbackResult::Error(NativeLoginError::Custom(message)) => {
1045                assert_eq!(message, "Browser closed");
1046            }
1047            _ => panic!("expected custom error"),
1048        }
1049    }
1050
1051    #[test]
1052    fn deserialize_payload_decodes_base64_json() {
1053        let payload = json!({
1054            "type": "success",
1055            "data": {
1056                "delegations": [],
1057                "userPublicKey": [],
1058                "authnMethod": "redirect"
1059            }
1060        })
1061        .to_string();
1062        let encoded = BASE64_STANDARD.encode(payload.as_bytes());
1063        let json = NativeAuthClient::deserialize_payload(&encoded).expect("decode payload");
1064        assert_eq!(json["type"], "success");
1065    }
1066
1067    #[test]
1068    fn normalize_delegations_handles_string_expiration() {
1069        let mut json = json!({
1070            "type": "success",
1071            "data": {
1072                "delegations": [{
1073                    "delegation": {
1074                        "expiration": "1763421459179717000",
1075                        "pubkey": [],
1076                        "targets": []
1077                    },
1078                    "signature": []
1079                }],
1080                "userPublicKey": [],
1081                "authnMethod": "native"
1082            }
1083        });
1084
1085        NativeAuthClient::normalize_delegations(&mut json).expect("normalize");
1086
1087        assert!(json["data"]["delegations"][0]["delegation"]["expiration"].is_number());
1088    }
1089}