ig_client/session/
auth.rs1use async_trait::async_trait;
4use reqwest::{Client, StatusCode};
5
6use crate::{
7 config::Config,
8 error::AuthError,
9 session::interface::{IgAuthenticator, IgSession},
10 session::response::SessionResp,
11};
12
13pub struct IgAuth<'a> {
15 cfg: &'a Config,
16 http: Client,
17}
18
19impl<'a> IgAuth<'a> {
20 pub fn new(cfg: &'a Config) -> Self {
28 Self {
29 cfg,
30 http: Client::builder()
31 .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
32 .build()
33 .expect("reqwest client"),
34 }
35 }
36
37 fn rest_url(&self, path: &str) -> String {
39 format!(
40 "{}/{}",
41 self.cfg.rest_api.base_url.trim_end_matches('/'),
42 path.trim_start_matches('/')
43 )
44 }
45
46 #[allow(dead_code)]
57 fn get_client(&self) -> &Client {
58 &self.http
59 }
60}
61
62#[async_trait]
63impl IgAuthenticator for IgAuth<'_> {
64 async fn login(&self) -> Result<IgSession, AuthError> {
65 let url = self.rest_url("session");
67
68 let api_key = self.cfg.credentials.api_key.trim();
70 let username = self.cfg.credentials.username.trim();
71 let password = self.cfg.credentials.password.trim();
72
73 tracing::info!("Login request to URL: {}", url);
75 tracing::info!("Using API key (length): {}", api_key.len());
76 tracing::info!("Using username: {}", username);
77
78 let body = serde_json::json!({
80 "identifier": username,
81 "password": password,
82 "encryptedPassword": false
83 });
84
85 tracing::debug!(
86 "Request body: {}",
87 serde_json::to_string(&body).unwrap_or_default()
88 );
89
90 let client = Client::builder()
92 .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
93 .build()
94 .expect("reqwest client");
95
96 let resp = client
98 .post(url)
99 .header("X-IG-API-KEY", api_key)
100 .header("Content-Type", "application/json; charset=UTF-8")
101 .header("Accept", "application/json; charset=UTF-8")
102 .header("Version", "2")
103 .json(&body)
104 .send()
105 .await?;
106
107 tracing::info!("Login response status: {}", resp.status());
109 tracing::debug!("Response headers: {:#?}", resp.headers());
110
111 match resp.status() {
112 StatusCode::OK => {
113 let cst = match resp.headers().get("CST") {
115 Some(value) => {
116 let cst_str = value
117 .to_str()
118 .map_err(|_| AuthError::Unexpected(StatusCode::OK))?;
119 tracing::info!(
120 "Successfully obtained CST token of length: {}",
121 cst_str.len()
122 );
123 cst_str.to_owned()
124 }
125 None => {
126 tracing::error!("CST header not found in response");
127 return Err(AuthError::Unexpected(StatusCode::OK));
128 }
129 };
130
131 let token = match resp.headers().get("X-SECURITY-TOKEN") {
132 Some(value) => {
133 let token_str = value
134 .to_str()
135 .map_err(|_| AuthError::Unexpected(StatusCode::OK))?;
136 tracing::info!(
137 "Successfully obtained X-SECURITY-TOKEN of length: {}",
138 token_str.len()
139 );
140 token_str.to_owned()
141 }
142 None => {
143 tracing::error!("X-SECURITY-TOKEN header not found in response");
144 return Err(AuthError::Unexpected(StatusCode::OK));
145 }
146 };
147
148 let json: SessionResp = resp.json().await?;
150 tracing::info!("Account ID: {}", json.account_id);
151
152 Ok(IgSession {
153 cst,
154 token,
155 account_id: json.account_id,
156 })
157 }
158 StatusCode::UNAUTHORIZED => {
159 tracing::error!("Authentication failed with UNAUTHORIZED");
160 let body = resp
161 .text()
162 .await
163 .unwrap_or_else(|_| "Could not read response body".to_string());
164 tracing::error!("Response body: {}", body);
165 Err(AuthError::BadCredentials)
166 }
167 StatusCode::FORBIDDEN => {
168 tracing::error!("Authentication failed with FORBIDDEN");
169 let body = resp
170 .text()
171 .await
172 .unwrap_or_else(|_| "Could not read response body".to_string());
173 tracing::error!("Response body: {}", body);
174 Err(AuthError::BadCredentials)
175 }
176 other => {
177 tracing::error!("Authentication failed with unexpected status: {}", other);
178 let body = resp
179 .text()
180 .await
181 .unwrap_or_else(|_| "Could not read response body".to_string());
182 tracing::error!("Response body: {}", body);
183 Err(AuthError::Unexpected(other))
184 }
185 }
186 }
187
188 async fn refresh(&self, sess: &IgSession) -> Result<IgSession, AuthError> {
189 let url = self.rest_url("session/refresh-token");
190
191 let api_key = self.cfg.credentials.api_key.trim();
193
194 tracing::info!("Refresh request to URL: {}", url);
196 tracing::info!("Using API key (length): {}", api_key.len());
197 tracing::info!("Using CST token (length): {}", sess.cst.len());
198 tracing::info!("Using X-SECURITY-TOKEN (length): {}", sess.token.len());
199
200 let client = Client::builder()
202 .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
203 .build()
204 .expect("reqwest client");
205
206 let resp = client
207 .post(url)
208 .header("X-IG-API-KEY", api_key)
209 .header("CST", &sess.cst)
210 .header("X-SECURITY-TOKEN", &sess.token)
211 .header("Version", "3")
212 .header("Content-Type", "application/json; charset=UTF-8")
213 .header("Accept", "application/json; charset=UTF-8")
214 .send()
215 .await?;
216
217 tracing::info!("Refresh response status: {}", resp.status());
219 tracing::debug!("Response headers: {:#?}", resp.headers());
220
221 match resp.status() {
222 StatusCode::OK => {
223 let cst = match resp.headers().get("CST") {
225 Some(value) => {
226 let cst_str = value
227 .to_str()
228 .map_err(|_| AuthError::Unexpected(StatusCode::OK))?;
229 tracing::info!(
230 "Successfully obtained refreshed CST token of length: {}",
231 cst_str.len()
232 );
233 cst_str.to_owned()
234 }
235 None => {
236 tracing::error!("CST header not found in refresh response");
237 return Err(AuthError::Unexpected(StatusCode::OK));
238 }
239 };
240
241 let token = match resp.headers().get("X-SECURITY-TOKEN") {
242 Some(value) => {
243 let token_str = value
244 .to_str()
245 .map_err(|_| AuthError::Unexpected(StatusCode::OK))?;
246 tracing::info!(
247 "Successfully obtained refreshed X-SECURITY-TOKEN of length: {}",
248 token_str.len()
249 );
250 token_str.to_owned()
251 }
252 None => {
253 tracing::error!("X-SECURITY-TOKEN header not found in refresh response");
254 return Err(AuthError::Unexpected(StatusCode::OK));
255 }
256 };
257
258 let json: SessionResp = resp.json().await?;
260 tracing::info!("Refreshed session for Account ID: {}", json.account_id);
261
262 Ok(IgSession {
263 cst,
264 token,
265 account_id: json.account_id,
266 })
267 }
268 other => {
269 tracing::error!("Session refresh failed with status: {}", other);
270 let body = resp
271 .text()
272 .await
273 .unwrap_or_else(|_| "Could not read response body".to_string());
274 tracing::error!("Response body: {}", body);
275 Err(AuthError::Unexpected(other))
276 }
277 }
278 }
279}