1use crate::constants::USER_AGENT;
4use crate::{
5 config::Config,
6 error::AuthError,
7 session::interface::{IgAuthenticator, IgSession},
8 session::response::{AccountSwitchRequest, AccountSwitchResponse, SessionResp, SessionV3Resp},
9 utils::rate_limiter::app_non_trading_limiter,
10};
11use async_trait::async_trait;
12use rand;
13use reqwest::{Client, StatusCode};
14use std::time::Duration;
15use tracing::{debug, error, info, trace, warn};
16
17pub struct IgAuth<'a> {
19 pub(crate) cfg: &'a Config,
20 http: Client,
21}
22
23impl<'a> IgAuth<'a> {
24 pub fn new(cfg: &'a Config) -> Self {
32 Self {
33 cfg,
34 http: Client::builder()
35 .user_agent(USER_AGENT)
36 .build()
37 .expect("reqwest client"),
38 }
39 }
40
41 fn rest_url(&self, path: &str) -> String {
43 format!(
44 "{}/{}",
45 self.cfg.rest_api.base_url.trim_end_matches('/'),
46 path.trim_start_matches('/')
47 )
48 }
49
50 #[allow(dead_code)]
61 fn get_client(&self) -> &Client {
62 &self.http
63 }
64}
65
66#[async_trait]
67impl IgAuthenticator for IgAuth<'_> {
68 async fn login(&self) -> Result<IgSession, AuthError> {
69 let api_version = self.cfg.api_version.unwrap_or(3);
72
73 debug!("Using API version {} for authentication", api_version);
74
75 match api_version {
76 2 => self.login_v2().await,
77 3 => self.login_v3().await,
78 _ => {
79 error!("Invalid API version: {}. Must be 2 or 3", api_version);
80 Err(AuthError::Unexpected(StatusCode::BAD_REQUEST))
81 }
82 }
83 }
84
85 async fn login_v2(&self) -> Result<IgSession, AuthError> {
86 const MAX_RETRIES: u32 = 3;
88 const INITIAL_RETRY_DELAY_MS: u64 = 10000; let mut retry_count = 0;
91 let mut retry_delay_ms = INITIAL_RETRY_DELAY_MS;
92
93 loop {
94 let limiter = app_non_trading_limiter();
96 limiter.wait().await;
97
98 let url = self.rest_url("session");
100
101 let api_key = self.cfg.credentials.api_key.trim();
103 let username = self.cfg.credentials.username.trim();
104 let password = self.cfg.credentials.password.trim();
105
106 debug!("Login v2 request to URL: {}", url);
108 debug!("Using API key (length): {}", api_key.len());
109 debug!("Using username: {}", username);
110
111 if retry_count > 0 {
112 debug!("Retry attempt {} of {}", retry_count, MAX_RETRIES);
113 }
114
115 let body = serde_json::json!({
117 "identifier": username,
118 "password": password,
119 "encryptedPassword": false
120 });
121
122 debug!(
123 "Request body: {}",
124 serde_json::to_string(&body).unwrap_or_default()
125 );
126
127 let client = Client::builder()
129 .user_agent(USER_AGENT)
130 .build()
131 .expect("reqwest client");
132
133 let resp = match client
135 .post(url.clone())
136 .header("X-IG-API-KEY", api_key)
137 .header("Content-Type", "application/json; charset=UTF-8")
138 .header("Accept", "application/json; charset=UTF-8")
139 .header("Version", "2")
140 .json(&body)
141 .send()
142 .await
143 {
144 Ok(resp) => resp,
145 Err(e) => {
146 error!("Failed to send login request: {}", e);
147 return Err(AuthError::Unexpected(StatusCode::INTERNAL_SERVER_ERROR));
148 }
149 };
150
151 debug!("Login v2 response status: {}", resp.status());
153 trace!("Response headers: {:#?}", resp.headers());
154
155 match resp.status() {
156 StatusCode::OK => {
157 let cst = match resp.headers().get("CST") {
159 Some(value) => {
160 let cst_str = value
161 .to_str()
162 .map_err(|_| AuthError::Unexpected(StatusCode::OK))?;
163 debug!(
164 "Successfully obtained CST token of length: {}",
165 cst_str.len()
166 );
167 cst_str.to_owned()
168 }
169 None => {
170 error!("CST header not found in response");
171 return Err(AuthError::Unexpected(StatusCode::OK));
172 }
173 };
174
175 let token = match resp.headers().get("X-SECURITY-TOKEN") {
176 Some(value) => {
177 let token_str = value
178 .to_str()
179 .map_err(|_| AuthError::Unexpected(StatusCode::OK))?;
180 debug!(
181 "Successfully obtained X-SECURITY-TOKEN of length: {}",
182 token_str.len()
183 );
184 token_str.to_owned()
185 }
186 None => {
187 error!("X-SECURITY-TOKEN header not found in response");
188 return Err(AuthError::Unexpected(StatusCode::OK));
189 }
190 };
191
192 let json: SessionResp = resp.json().await?;
194 let account_id = json.account_id.clone();
195
196 let session =
199 IgSession::from_config(cst.clone(), token.clone(), account_id, self.cfg);
200
201 if let Some(stats) = session.get_rate_limit_stats().await {
203 debug!("Rate limiter initialized: {}", stats);
204 }
205
206 return Ok(session);
207 }
208 StatusCode::UNAUTHORIZED => {
209 error!("Authentication failed with UNAUTHORIZED");
210 let body = resp
211 .text()
212 .await
213 .unwrap_or_else(|_| "Could not read response body".to_string());
214 error!("Response body: {}", body);
215 return Err(AuthError::BadCredentials);
216 }
217 StatusCode::FORBIDDEN => {
218 error!("Authentication failed with FORBIDDEN");
219 let body = resp
220 .text()
221 .await
222 .unwrap_or_else(|_| "Could not read response body".to_string());
223
224 if body.contains("exceeded-api-key-allowance") {
225 error!("Rate Limit Exceeded: {}", &body);
226
227 if retry_count < MAX_RETRIES {
229 retry_count += 1;
230 let jitter = rand::random::<u64>() % 5000;
232 let delay = retry_delay_ms + jitter;
233 warn!(
234 "Rate limit exceeded. Retrying in {} ms (attempt {} of {})",
235 delay, retry_count, MAX_RETRIES
236 );
237
238 tokio::time::sleep(Duration::from_millis(delay)).await;
239
240 retry_delay_ms *= 2; continue;
243 } else {
244 error!(
245 "Maximum retry attempts ({}) reached. Giving up.",
246 MAX_RETRIES
247 );
248 return Err(AuthError::RateLimitExceeded);
249 }
250 }
251
252 error!("Response body: {}", body);
253 return Err(AuthError::BadCredentials);
254 }
255 other => {
256 error!("Authentication failed with unexpected status: {}", other);
257 let body = resp
258 .text()
259 .await
260 .unwrap_or_else(|_| "Could not read response body".to_string());
261 error!("Response body: {}", body);
262 return Err(AuthError::Unexpected(other));
263 }
264 }
265 }
266 }
267
268 async fn login_v3(&self) -> Result<IgSession, AuthError> {
269 const MAX_RETRIES: u32 = 3;
271 const INITIAL_RETRY_DELAY_MS: u64 = 10000; let mut retry_count = 0;
274 let mut retry_delay_ms = INITIAL_RETRY_DELAY_MS;
275
276 loop {
277 let limiter = app_non_trading_limiter();
279 limiter.wait().await;
280
281 let url = self.rest_url("session");
282
283 let api_key = self.cfg.credentials.api_key.trim();
285 let username = self.cfg.credentials.username.trim();
286 let password = self.cfg.credentials.password.trim();
287
288 debug!("Login v3 request to URL: {}", url);
290 debug!("Using API key (length): {}", api_key.len());
291 debug!("Using username: {}", username);
292
293 if retry_count > 0 {
294 debug!("Retry attempt {} of {}", retry_count, MAX_RETRIES);
295 }
296
297 let body = serde_json::json!({
299 "identifier": username,
300 "password": password,
301 "encryptedPassword": null
302 });
303
304 debug!(
305 "Request body: {}",
306 serde_json::to_string(&body).unwrap_or_default()
307 );
308
309 let client = Client::builder()
311 .user_agent(USER_AGENT)
312 .build()
313 .expect("reqwest client");
314
315 let resp = match client
317 .post(url.clone())
318 .header("X-IG-API-KEY", api_key)
319 .header("Content-Type", "application/json")
320 .header("Version", "3")
321 .json(&body)
322 .send()
323 .await
324 {
325 Ok(resp) => resp,
326 Err(e) => {
327 error!("Failed to send login v3 request: {}", e);
328 return Err(AuthError::Unexpected(StatusCode::INTERNAL_SERVER_ERROR));
329 }
330 };
331
332 debug!("Login v3 response status: {}", resp.status());
334 trace!("Response headers: {:#?}", resp.headers());
335
336 match resp.status() {
337 StatusCode::OK => {
338 let json: SessionV3Resp = resp.json().await?;
340
341 debug!("Successfully authenticated with OAuth");
342 debug!("Account ID: {}", json.account_id);
343 debug!("Client ID: {}", json.client_id);
344 debug!("Lightstreamer endpoint: {}", json.lightstreamer_endpoint);
345 debug!(
346 "Access token length: {}",
347 json.oauth_token.access_token.len()
348 );
349 debug!("Token expires in: {} seconds", json.oauth_token.expires_in);
350
351 let session = IgSession::from_oauth(
353 json.oauth_token,
354 json.account_id,
355 json.client_id,
356 json.lightstreamer_endpoint,
357 self.cfg,
358 );
359
360 if let Some(stats) = session.get_rate_limit_stats().await {
362 debug!("Rate limiter initialized: {}", stats);
363 }
364
365 return Ok(session);
366 }
367 StatusCode::UNAUTHORIZED => {
368 error!("Authentication failed with UNAUTHORIZED");
369 let body = resp
370 .text()
371 .await
372 .unwrap_or_else(|_| "Could not read response body".to_string());
373 error!("Response body: {}", body);
374 return Err(AuthError::BadCredentials);
375 }
376 StatusCode::FORBIDDEN => {
377 error!("Authentication failed with FORBIDDEN");
378 let body = resp
379 .text()
380 .await
381 .unwrap_or_else(|_| "Could not read response body".to_string());
382
383 if body.contains("exceeded-api-key-allowance") {
384 error!("Rate Limit Exceeded: {}", &body);
385
386 if retry_count < MAX_RETRIES {
387 retry_count += 1;
388 let jitter = rand::random::<u64>() % 5000;
389 let delay = retry_delay_ms + jitter;
390 warn!(
391 "Rate limit exceeded. Retrying in {} ms (attempt {} of {})",
392 delay, retry_count, MAX_RETRIES
393 );
394
395 tokio::time::sleep(Duration::from_millis(delay)).await;
396 retry_delay_ms *= 2;
397 continue;
398 } else {
399 error!(
400 "Maximum retry attempts ({}) reached. Giving up.",
401 MAX_RETRIES
402 );
403 return Err(AuthError::RateLimitExceeded);
404 }
405 }
406
407 error!("Response body: {}", body);
408 return Err(AuthError::BadCredentials);
409 }
410 other => {
411 error!("Authentication failed with unexpected status: {}", other);
412 let body = resp
413 .text()
414 .await
415 .unwrap_or_else(|_| "Could not read response body".to_string());
416 error!("Response body: {}", body);
417 return Err(AuthError::Unexpected(other));
418 }
419 }
420 }
421 }
422
423 async fn refresh(&self, sess: &IgSession) -> Result<IgSession, AuthError> {
425 let url = self.rest_url("session/refresh-token");
426
427 let api_key = self.cfg.credentials.api_key.trim();
429
430 debug!("Refresh request to URL: {}", url);
432 debug!("Using API key (length): {}", api_key.len());
433 debug!("Using CST token (length): {}", sess.cst.len());
434 debug!("Using X-SECURITY-TOKEN (length): {}", sess.token.len());
435
436 let client = Client::builder()
438 .user_agent(USER_AGENT)
439 .build()
440 .expect("reqwest client");
441
442 let resp = client
443 .post(url)
444 .header("X-IG-API-KEY", api_key)
445 .header("CST", &sess.cst)
446 .header("X-SECURITY-TOKEN", &sess.token)
447 .header("Version", "3")
448 .header("Content-Type", "application/json; charset=UTF-8")
449 .header("Accept", "application/json; charset=UTF-8")
450 .send()
451 .await?;
452
453 debug!("Refresh response status: {}", resp.status());
455 trace!("Response headers: {:#?}", resp.headers());
456
457 match resp.status() {
458 StatusCode::OK => {
459 let cst = match resp.headers().get("CST") {
461 Some(value) => {
462 let cst_str = value
463 .to_str()
464 .map_err(|_| AuthError::Unexpected(StatusCode::OK))?;
465 debug!(
466 "Successfully obtained refreshed CST token of length: {}",
467 cst_str.len()
468 );
469 cst_str.to_owned()
470 }
471 None => {
472 error!("CST header not found in refresh response");
473 return Err(AuthError::Unexpected(StatusCode::OK));
474 }
475 };
476
477 let token = match resp.headers().get("X-SECURITY-TOKEN") {
478 Some(value) => {
479 let token_str = value
480 .to_str()
481 .map_err(|_| AuthError::Unexpected(StatusCode::OK))?;
482 debug!(
483 "Successfully obtained refreshed X-SECURITY-TOKEN of length: {}",
484 token_str.len()
485 );
486 token_str.to_owned()
487 }
488 None => {
489 error!("X-SECURITY-TOKEN header not found in refresh response");
490 return Err(AuthError::Unexpected(StatusCode::OK));
491 }
492 };
493
494 let json: SessionResp = resp.json().await?;
496 debug!("Refreshed session for Account ID: {}", json.account_id);
497
498 Ok(IgSession::from_config(
500 cst,
501 token,
502 json.account_id,
503 self.cfg,
504 ))
505 }
506 other => {
507 error!("Session refresh failed with status: {}", other);
508 let body = resp
509 .text()
510 .await
511 .unwrap_or_else(|_| "Could not read response body".to_string());
512 error!("Response body: {}", body);
513 Err(AuthError::Unexpected(other))
514 }
515 }
516 }
517
518 async fn switch_account(
519 &self,
520 session: &IgSession,
521 account_id: &str,
522 default_account: Option<bool>,
523 ) -> Result<IgSession, AuthError> {
524 if session.account_id == account_id {
526 debug!("Already on account ID: {}. No need to switch.", account_id);
527 return Ok(session.clone());
529 }
530
531 let url = self.rest_url("session");
532 let api_key = self.cfg.credentials.api_key.trim();
533
534 debug!("Account switch request to URL: {}", url);
536 debug!("Using API key (length): {}", api_key.len());
537 debug!("Switching to account ID: {}", account_id);
538 debug!("Set as default account: {:?}", default_account);
539
540 let body = AccountSwitchRequest {
542 account_id: account_id.to_string(),
543 default_account,
544 };
545
546 trace!(
547 "Request body: {}",
548 serde_json::to_string(&body).unwrap_or_default()
549 );
550
551 let client = Client::builder()
553 .user_agent(USER_AGENT)
554 .build()
555 .expect("reqwest client");
556
557 let mut request = client
559 .put(url)
560 .header("X-IG-API-KEY", api_key)
561 .header("Version", "1")
562 .header("Content-Type", "application/json; charset=UTF-8")
563 .header("Accept", "application/json; charset=UTF-8");
564
565 if let Some(oauth_token) = &session.oauth_token {
567 debug!("Using OAuth authentication for account switch");
569 request = request
570 .header(
571 "Authorization",
572 format!("Bearer {}", oauth_token.access_token),
573 )
574 .header("IG-ACCOUNT-ID", &session.account_id);
575 } else {
576 debug!("Using CST authentication for account switch");
578 request = request
579 .header("CST", &session.cst)
580 .header("X-SECURITY-TOKEN", &session.token);
581 }
582
583 let resp = request.json(&body).send().await?;
584
585 debug!("Account switch response status: {}", resp.status());
587 trace!("Response headers: {:#?}", resp.headers());
588
589 match resp.status() {
590 StatusCode::OK => {
591 let new_cst = match resp.headers().get("CST") {
598 Some(value) => {
599 let cst_str = value
600 .to_str()
601 .map_err(|_| AuthError::Unexpected(StatusCode::OK))?;
602 debug!(
603 "Successfully obtained new CST token of length: {}",
604 cst_str.len()
605 );
606 cst_str.to_owned()
607 }
608 None => {
609 warn!("CST header not found in switch response, using existing token");
610 session.cst.clone()
611 }
612 };
613
614 let new_token = match resp.headers().get("X-SECURITY-TOKEN") {
615 Some(value) => {
616 let token_str = value
617 .to_str()
618 .map_err(|_| AuthError::Unexpected(StatusCode::OK))?;
619 debug!(
620 "Successfully obtained new X-SECURITY-TOKEN of length: {}",
621 token_str.len()
622 );
623 token_str.to_owned()
624 }
625 None => {
626 warn!(
627 "X-SECURITY-TOKEN header not found in switch response, using existing token"
628 );
629 return Err(AuthError::Unexpected(StatusCode::NO_CONTENT));
630 }
631 };
632
633 let switch_response: AccountSwitchResponse = resp.json().await?;
635 info!("Account switch successful to: {}", account_id);
636 trace!("Account switch response: {:?}", switch_response);
637
638 if session.oauth_token.is_some() {
642 let mut new_session = session.clone();
644 new_session.account_id = account_id.to_string();
645 Ok(new_session)
646 } else {
647 Ok(IgSession::from_config(
649 new_cst,
650 new_token,
651 account_id.to_string(),
652 self.cfg,
653 ))
654 }
655 }
656 other => {
657 error!("Account switch failed with status: {}", other);
658 let body = resp
659 .text()
660 .await
661 .unwrap_or_else(|_| "Could not read response body".to_string());
662 error!("Response body: {}", body);
663
664 if other == StatusCode::UNAUTHORIZED {
667 warn!(
668 "Cannot switch to account ID: {}. The account might not exist or you don't have permission.",
669 account_id
670 );
671 }
672
673 Err(AuthError::Unexpected(other))
674 }
675 }
676 }
677
678 async fn relogin(&self, session: &IgSession) -> Result<IgSession, AuthError> {
679 let margin = chrono::Duration::minutes(30);
681
682 let is_expired = {
683 let timer = session.token_timer.lock().unwrap();
684 timer.is_expired_w_margin(margin)
685 };
686
687 if is_expired {
688 info!("Tokens are expired or close to expiring, performing re-login");
689 self.login().await
690 } else {
691 debug!("Tokens are still valid, reusing existing session");
692 Ok(session.clone())
693 }
694 }
695
696 async fn relogin_and_switch_account(
697 &self,
698 session: &IgSession,
699 account_id: &str,
700 default_account: Option<bool>,
701 ) -> Result<IgSession, AuthError> {
702 let session = self.relogin(session).await?;
703 debug!(
704 "Relogin check completed for account: {}, trying to switch to {}",
705 session.account_id, account_id
706 );
707
708 match self
709 .switch_account(&session, account_id, default_account)
710 .await
711 {
712 Ok(new_session) => Ok(new_session),
713 Err(e) => {
714 warn!("Could not switch to account {}: {:?}.", account_id, e);
715 Err(e)
716 }
717 }
718 }
719
720 async fn login_and_switch_account(
721 &self,
722 account_id: &str,
723 default_account: Option<bool>,
724 ) -> Result<IgSession, AuthError> {
725 let session = self.login().await?;
726 self.relogin_and_switch_account(&session, account_id, default_account)
727 .await
728 }
729}