1use reqwest::header::HeaderValue;
7use serde_json::Value;
8
9use crate::client::{DhanClient, required_query_value};
10use crate::constants::AUTH_BASE_URL;
11use crate::error::{ApiErrorBody, DhanError, Result};
12use crate::types::auth::{AppConsentResponse, PartnerConsentResponse, TokenResponse};
13
14impl DhanClient {
15 pub async fn generate_access_token(
44 client_id: &str,
45 pin: &str,
46 totp: &str,
47 ) -> Result<TokenResponse> {
48 let client_id = required_query_value("client_id", client_id)?;
49 let pin = required_query_value("pin", pin)?;
50 let totp = required_query_value("totp", totp)?;
51 let url = format!(
52 "{}/app/generateAccessToken?dhanClientId={}&pin={}&totp={}",
53 AUTH_BASE_URL, client_id, pin, totp
54 );
55
56 tracing::debug!("POST generate_access_token");
57
58 let http = auth_http_client()?;
59 let resp = http
60 .post(&url)
61 .send()
62 .await
63 .map_err(sanitize_auth_http_error)?;
64
65 let status = resp.status();
66 let body = resp
67 .text()
68 .await
69 .map_err(|source| DhanError::ResponseBody {
70 status,
71 source: source.without_url(),
72 })?;
73
74 if status.is_success() {
75 serde_json::from_str(&body).map_err(DhanError::Json)
76 } else {
77 if let Ok(api_err) = serde_json::from_str::<ApiErrorBody>(&body) {
78 if api_err.error_code.is_some() || api_err.error_message.is_some() {
79 return Err(DhanError::Api(api_err));
80 }
81 }
82 Err(DhanError::HttpStatus { status, body })
83 }
84 }
85
86 pub async fn renew_token(&mut self) -> Result<TokenResponse> {
103 let url = format!("{}/v2/RenewToken", self.base_url());
104
105 tracing::debug!("GET renew_token");
106
107 let mut access_token = HeaderValue::from_str(self.access_token())?;
108 access_token.set_sensitive(true);
109 let mut client_id = HeaderValue::from_str(self.client_id())?;
110 client_id.set_sensitive(true);
111
112 let resp = self
113 .http()
114 .get(&url)
115 .header("access-token", access_token)
116 .header("dhanClientId", client_id)
117 .header("Content-Type", "application/json")
118 .header("Accept", "application/json")
119 .send()
120 .await
121 .map_err(sanitize_auth_http_error)?;
122
123 let status = resp.status();
124 let bytes = resp
125 .bytes()
126 .await
127 .map_err(|source| DhanError::ResponseBody {
128 status,
129 source: source.without_url(),
130 })?;
131
132 if status.is_success() {
133 let token: TokenResponse = serde_json::from_slice(&bytes).map_err(DhanError::Json)?;
134 self.try_set_access_token(&token.access_token)?;
136 Ok(token)
137 } else {
138 let body = String::from_utf8_lossy(&bytes);
139 Err(self.parse_error_body(status, &body))
140 }
141 }
142
143 pub async fn generate_consent(
156 client_id: &str,
157 app_id: &str,
158 app_secret: &str,
159 ) -> Result<AppConsentResponse> {
160 let client_id = required_query_value("client_id", client_id)?;
161 let url = format!(
162 "{}/app/generate-consent?client_id={}",
163 AUTH_BASE_URL, client_id
164 );
165
166 tracing::debug!("POST generate_consent");
167
168 let http = auth_http_client()?;
169 let resp = http
170 .post(&url)
171 .header("app_id", sensitive_header_value(app_id)?)
172 .header("app_secret", sensitive_header_value(app_secret)?)
173 .send()
174 .await
175 .map_err(sanitize_auth_http_error)?;
176
177 let status = resp.status();
178 let body = resp
179 .text()
180 .await
181 .map_err(|source| DhanError::ResponseBody {
182 status,
183 source: source.without_url(),
184 })?;
185
186 if status.is_success() {
187 serde_json::from_str(&body).map_err(DhanError::Json)
188 } else {
189 Self::parse_auth_error(status, &body)
190 }
191 }
192
193 pub fn consent_login_url(consent_app_id: &str) -> String {
205 let consent_app_id =
206 url::form_urlencoded::byte_serialize(consent_app_id.as_bytes()).collect::<String>();
207 format!(
208 "{}/login/consentApp-login?consentAppId={}",
209 AUTH_BASE_URL, consent_app_id
210 )
211 }
212
213 pub async fn consume_consent(
220 token_id: &str,
221 app_id: &str,
222 app_secret: &str,
223 ) -> Result<TokenResponse> {
224 let token_id = required_query_value("token_id", token_id)?;
225 let url = format!(
226 "{}/app/consumeApp-consent?tokenId={}",
227 AUTH_BASE_URL, token_id
228 );
229
230 tracing::debug!("POST consume_consent");
231
232 let http = auth_http_client()?;
233 let resp = http
234 .post(&url)
235 .header("app_id", sensitive_header_value(app_id)?)
236 .header("app_secret", sensitive_header_value(app_secret)?)
237 .send()
238 .await
239 .map_err(sanitize_auth_http_error)?;
240
241 let status = resp.status();
242 let body = resp
243 .text()
244 .await
245 .map_err(|source| DhanError::ResponseBody {
246 status,
247 source: source.without_url(),
248 })?;
249
250 if status.is_success() {
251 serde_json::from_str(&body).map_err(DhanError::Json)
252 } else {
253 Self::parse_auth_error(status, &body)
254 }
255 }
256
257 pub async fn partner_generate_consent(
265 partner_id: &str,
266 partner_secret: &str,
267 ) -> Result<PartnerConsentResponse> {
268 let url = format!("{}/partner/generate-consent", AUTH_BASE_URL);
269
270 tracing::debug!("POST partner_generate_consent");
271
272 let http = auth_http_client()?;
273 let resp = http
274 .post(&url)
275 .header("partner_id", sensitive_header_value(partner_id)?)
276 .header("partner_secret", sensitive_header_value(partner_secret)?)
277 .send()
278 .await
279 .map_err(sanitize_auth_http_error)?;
280
281 let status = resp.status();
282 let body = resp
283 .text()
284 .await
285 .map_err(|source| DhanError::ResponseBody {
286 status,
287 source: source.without_url(),
288 })?;
289
290 if status.is_success() {
291 serde_json::from_str(&body).map_err(DhanError::Json)
292 } else {
293 Self::parse_auth_error(status, &body)
294 }
295 }
296
297 pub fn partner_consent_login_url(consent_id: &str) -> String {
302 let consent_id =
303 url::form_urlencoded::byte_serialize(consent_id.as_bytes()).collect::<String>();
304 format!("{}/consent-login?consentId={consent_id}", AUTH_BASE_URL)
305 }
306
307 pub async fn partner_consume_consent(
311 token_id: &str,
312 partner_id: &str,
313 partner_secret: &str,
314 ) -> Result<TokenResponse> {
315 let token_id = required_query_value("token_id", token_id)?;
316 let url = format!(
317 "{}/partner/consume-consent?tokenId={}",
318 AUTH_BASE_URL, token_id
319 );
320
321 tracing::debug!("POST partner_consume_consent");
322
323 let http = auth_http_client()?;
324 let resp = http
325 .post(&url)
326 .header("partner_id", sensitive_header_value(partner_id)?)
327 .header("partner_secret", sensitive_header_value(partner_secret)?)
328 .send()
329 .await
330 .map_err(sanitize_auth_http_error)?;
331
332 let status = resp.status();
333 let body = resp
334 .text()
335 .await
336 .map_err(|source| DhanError::ResponseBody {
337 status,
338 source: source.without_url(),
339 })?;
340
341 if status.is_success() {
342 serde_json::from_str(&body).map_err(DhanError::Json)
343 } else {
344 Self::parse_auth_error(status, &body)
345 }
346 }
347
348 fn parse_auth_error<T>(status: reqwest::StatusCode, body: &str) -> Result<T> {
354 if let Ok(api_err) = serde_json::from_str::<ApiErrorBody>(body) {
355 if api_err.error_code.is_some() || api_err.error_message.is_some() {
356 return Err(DhanError::Api(api_err));
357 }
358 }
359 if let Ok(val) = serde_json::from_str::<Value>(body) {
361 if let Some(status_str) = val.get("status").and_then(|v| v.as_str()) {
362 return Err(DhanError::HttpStatus {
363 status,
364 body: format!("auth error: {status_str}"),
365 });
366 }
367 }
368 Err(DhanError::HttpStatus {
369 status,
370 body: body.to_owned(),
371 })
372 }
373}
374
375fn sensitive_header_value(value: &str) -> Result<HeaderValue> {
376 let mut value = HeaderValue::from_str(value)?;
377 value.set_sensitive(true);
378 Ok(value)
379}
380
381fn auth_http_client() -> Result<reqwest::Client> {
382 reqwest::Client::builder()
383 .redirect(reqwest::redirect::Policy::none())
384 .build()
385 .map_err(DhanError::Http)
386}
387
388fn sanitize_auth_http_error(error: reqwest::Error) -> DhanError {
389 DhanError::Http(error.without_url())
390}
391
392#[cfg(test)]
393mod tests {
394 use super::sanitize_auth_http_error;
395
396 #[tokio::test]
397 async fn auth_transport_errors_do_not_retain_sensitive_urls() {
398 let pin = "1234";
399 let totp = "654321";
400 let token_id = "sensitive-consent-token";
401 let url = format!(
402 "ftp://127.0.0.1/app/generateAccessToken?dhanClientId=1&pin={pin}&totp={totp}&tokenId={token_id}"
403 );
404 let error = reqwest::Client::new()
405 .get(url)
406 .send()
407 .await
408 .expect_err("reqwest must reject the unsupported URL scheme");
409 let error = sanitize_auth_http_error(error);
410 let display = error.to_string();
411 let debug = format!("{error:?}");
412 for secret in [pin, totp, token_id] {
413 assert!(!display.contains(secret));
414 assert!(!debug.contains(secret));
415 }
416 }
417}