kiteconnect_async_wasm/connect/auth.rs
1//! # Authentication Module
2//!
3//! This module contains authentication-related methods for the KiteConnect API.
4
5use crate::connect::endpoints::KiteEndpoint;
6use anyhow::{anyhow, Result};
7use serde_json::Value as JsonValue;
8use std::collections::HashMap;
9
10// Import typed models for dual API support
11use crate::models::auth::{SessionData, UserProfile};
12use crate::models::common::KiteResult;
13
14// Native platform imports
15#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
16use sha2::{Digest, Sha256};
17
18// WASM platform imports
19#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
20use web_sys::window;
21
22#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
23use js_sys::Uint8Array;
24
25#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
26use wasm_bindgen_futures::JsFuture;
27
28use crate::connect::KiteConnect;
29
30impl KiteConnect {
31 // === LEGACY API METHODS (JSON responses) ===
32
33 /// Generates the KiteConnect login URL for user authentication
34 ///
35 /// This URL should be opened in a browser to allow the user to log in to their
36 /// Zerodha account. After successful login, the user will be redirected to your
37 /// redirect URL with a `request_token` parameter.
38 ///
39 /// # Returns
40 ///
41 /// A login URL string that can be opened in a browser
42 ///
43 /// # Example
44 ///
45 /// ```rust
46 /// use kiteconnect_async_wasm::connect::KiteConnect;
47 ///
48 /// let client = KiteConnect::new("your_api_key", "");
49 /// let login_url = client.login_url();
50 ///
51 /// println!("Please visit: {}", login_url);
52 /// // User visits URL, logs in, and is redirected with request_token
53 /// ```
54 ///
55 /// # Authentication Flow
56 ///
57 /// 1. Generate login URL with this method
58 /// 2. Direct user to the URL in a browser
59 /// 3. User completes login and is redirected with `request_token` parameter
60 /// 4. Use `generate_session()` with the request token to get access token
61 pub fn login_url(&self) -> String {
62 format!(
63 "https://kite.trade/connect/login?api_key={}&v3",
64 self.api_key
65 )
66 }
67
68 /// Compute checksum for authentication - different implementations for native vs WASM
69 #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
70 async fn compute_checksum(&self, input: &str) -> Result<String> {
71 let mut hasher = Sha256::new();
72 hasher.update(input.as_bytes());
73 let result = hasher.finalize();
74 Ok(hex::encode(result))
75 }
76
77 #[cfg(all(feature = "wasm", target_arch = "wasm32"))]
78 async fn compute_checksum(&self, input: &str) -> Result<String> {
79 // WASM implementation using Web Crypto API
80 let window = window().ok_or_else(|| anyhow!("No window object"))?;
81 let crypto = window.crypto().map_err(|_| anyhow!("No crypto object"))?;
82 let subtle = crypto.subtle();
83
84 let data = Uint8Array::from(input.as_bytes());
85 let digest_promise = subtle
86 .digest_with_str_and_u8_array("SHA-256", &data.to_vec())
87 .map_err(|_| anyhow!("Failed to create digest"))?;
88
89 let digest_result = JsFuture::from(digest_promise)
90 .await
91 .map_err(|_| anyhow!("Failed to compute hash"))?;
92
93 let digest_array = Uint8Array::new(&digest_result);
94 let digest_vec: Vec<u8> = digest_array.to_vec();
95 Ok(hex::encode(digest_vec))
96 }
97
98 /// Fallback checksum implementation when neither native nor wasm features are enabled
99 #[cfg(not(any(
100 all(feature = "native", not(target_arch = "wasm32")),
101 all(feature = "wasm", target_arch = "wasm32")
102 )))]
103 async fn compute_checksum(&self, _input: &str) -> Result<String> {
104 Err(anyhow!(
105 "Checksum computation requires either 'native' or 'wasm' feature to be enabled"
106 ))
107 }
108
109 /// Generates an access token using the request token from login
110 ///
111 /// This method completes the authentication flow by exchanging the request token
112 /// (obtained after user login) for an access token that can be used for API calls.
113 /// The access token is automatically stored in the client instance.
114 ///
115 /// # Arguments
116 ///
117 /// * `request_token` - The request token received after user login
118 /// * `api_secret` - Your KiteConnect API secret
119 ///
120 /// # Returns
121 ///
122 /// A `Result<JsonValue>` containing the session information including access token
123 ///
124 /// # Errors
125 ///
126 /// Returns an error if:
127 /// - The request token is invalid or expired
128 /// - The API secret is incorrect
129 /// - Network request fails
130 /// - Response parsing fails
131 ///
132 /// # Example
133 ///
134 /// ```rust,no_run
135 /// use kiteconnect_async_wasm::connect::KiteConnect;
136 ///
137 /// # #[tokio::main]
138 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
139 /// let mut client = KiteConnect::new("your_api_key", "");
140 ///
141 /// // After user completes login and you receive the request_token
142 /// let session_data = client
143 /// .generate_session("request_token_from_callback", "your_api_secret")
144 /// .await?;
145 ///
146 /// println!("Session created: {:?}", session_data);
147 /// // Access token is now automatically set in the client
148 /// # Ok(())
149 /// # }
150 /// ```
151 ///
152 /// # Authentication Flow
153 ///
154 /// 1. Call `login_url()` to get login URL
155 /// 2. User visits URL and completes login
156 /// 3. User is redirected with `request_token` parameter
157 /// 4. Call this method with the request token and API secret
158 /// 5. Access token is automatically set for subsequent API calls
159 pub async fn generate_session(
160 &mut self,
161 request_token: &str,
162 api_secret: &str,
163 ) -> Result<JsonValue> {
164 // Create a hex digest from api key, request token, api secret
165 let input = format!("{}{}{}", self.api_key, request_token, api_secret);
166 let checksum = self.compute_checksum(&input).await?;
167
168 let api_key: &str = &self.api_key.clone();
169 let mut data = HashMap::new();
170 data.insert("api_key", api_key);
171 data.insert("request_token", request_token);
172 data.insert("checksum", checksum.as_str());
173
174 let resp = self
175 .send_request_with_rate_limiting_and_retry(
176 KiteEndpoint::GenerateSession,
177 &[],
178 None,
179 Some(data),
180 )
181 .await
182 .map_err(|e| anyhow!("Generate session failed: {:?}", e))?;
183
184 if resp.status().is_success() {
185 let jsn: JsonValue = resp.json().await?;
186 self.set_access_token(jsn["data"]["access_token"].as_str().unwrap());
187 Ok(jsn)
188 } else {
189 let error_text: String = resp.text().await?;
190 Err(anyhow!(error_text))
191 }
192 }
193
194 /// Invalidates the access token
195 ///
196 /// This call invalidates the access_token and destroys the API session. After this,
197 /// the user should be sent through a new login flow before further interactions.
198 /// This does not log the user out of the official Kite web or mobile applications.
199 ///
200 /// # Arguments
201 ///
202 /// * `access_token` - The access token to invalidate
203 ///
204 /// # Returns
205 ///
206 /// A `Result<JsonValue>` containing the success response
207 ///
208 /// # Example
209 ///
210 /// ```rust,no_run
211 /// use kiteconnect_async_wasm::connect::KiteConnect;
212 ///
213 /// # #[tokio::main]
214 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
215 /// let client = KiteConnect::new("your_api_key", "access_token");
216 ///
217 /// let result = client.invalidate_access_token("access_token").await?;
218 /// println!("Session invalidated: {:?}", result);
219 /// # Ok(())
220 /// # }
221 /// ```
222 pub async fn invalidate_access_token(&self, access_token: &str) -> Result<JsonValue> {
223 // For invalidate session, the API expects query parameters, not form data
224 let query_params = vec![
225 ("api_key", self.api_key.as_str()),
226 ("access_token", access_token),
227 ];
228
229 let resp = self
230 .send_request_with_rate_limiting_and_retry(
231 KiteEndpoint::InvalidateSession,
232 &[],
233 Some(query_params),
234 None, // No form data for DELETE request
235 )
236 .await
237 .map_err(|e| anyhow!("Invalidate access token failed: {:?}", e))?;
238
239 if resp.status().is_success() {
240 let jsn: JsonValue = resp.json().await?;
241 Ok(jsn)
242 } else {
243 let error_text: String = resp.text().await?;
244 Err(anyhow!(error_text))
245 }
246 }
247
248 /// Request for new access token
249 pub async fn renew_access_token(
250 &mut self,
251 access_token: &str,
252 api_secret: &str,
253 ) -> Result<JsonValue> {
254 // Create a hex digest from api key, request token, api secret
255 let input = format!("{}{}{}", self.api_key, access_token, api_secret);
256 let checksum = self.compute_checksum(&input).await?;
257
258 let api_key: &str = &self.api_key.clone();
259 let mut data = HashMap::new();
260 data.insert("api_key", api_key);
261 data.insert("access_token", access_token);
262 data.insert("checksum", checksum.as_str());
263
264 let resp = self
265 .send_request_with_rate_limiting_and_retry(
266 KiteEndpoint::RenewAccessToken,
267 &[],
268 None,
269 Some(data),
270 )
271 .await
272 .map_err(|e| anyhow!("Renew access token failed: {:?}", e))?;
273
274 if resp.status().is_success() {
275 let jsn: JsonValue = resp.json().await?;
276 self.set_access_token(jsn["access_token"].as_str().unwrap());
277 Ok(jsn)
278 } else {
279 let error_text: String = resp.text().await?;
280 Err(anyhow!(error_text))
281 }
282 }
283
284 /// Invalidates the refresh token
285 pub async fn invalidate_refresh_token(&self, refresh_token: &str) -> Result<reqwest::Response> {
286 let mut data = HashMap::new();
287 data.insert("refresh_token", refresh_token);
288
289 self.send_request_with_rate_limiting_and_retry(
290 KiteEndpoint::InvalidateRefreshToken,
291 &[],
292 None,
293 Some(data),
294 )
295 .await
296 .map_err(|e| anyhow!("Invalidate refresh token failed: {:?}", e))
297 }
298
299 // === TYPED API METHODS (v1.0.0) ===
300
301 /// Generates session with typed response
302 ///
303 /// Returns strongly typed session data instead of JsonValue.
304 /// This is the preferred method for new applications.
305 ///
306 /// # Arguments
307 ///
308 /// * `request_token` - The request token received after user login
309 /// * `api_secret` - Your KiteConnect API secret
310 ///
311 /// # Returns
312 ///
313 /// A `KiteResult<SessionData>` containing typed session information
314 ///
315 /// # Example
316 ///
317 /// ```rust,no_run
318 /// use kiteconnect_async_wasm::connect::KiteConnect;
319 ///
320 /// # #[tokio::main]
321 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
322 /// let mut client = KiteConnect::new("your_api_key", "");
323 ///
324 /// let session = client.generate_session_typed("request_token", "api_secret").await?;
325 /// println!("Access token: {}", session.access_token);
326 /// println!("User ID: {}", session.user_id);
327 /// # Ok(())
328 /// # }
329 /// ```
330 pub async fn generate_session_typed(
331 &mut self,
332 request_token: &str,
333 api_secret: &str,
334 ) -> KiteResult<SessionData> {
335 let json_response = self
336 .generate_session(request_token, api_secret)
337 .await
338 .map_err(crate::models::common::KiteError::Legacy)?;
339
340 // Extract the data field from response
341 let data = json_response["data"].clone();
342 self.parse_response(data)
343 }
344
345 /// Get user profile with typed response
346 ///
347 /// Returns strongly typed user profile data instead of JsonValue.
348 ///
349 /// # Returns
350 ///
351 /// A `KiteResult<UserProfile>` containing typed user profile information
352 ///
353 /// # Example
354 ///
355 /// ```rust,no_run
356 /// use kiteconnect_async_wasm::connect::KiteConnect;
357 ///
358 /// # #[tokio::main]
359 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
360 /// let client = KiteConnect::new("api_key", "access_token");
361 ///
362 /// let profile = client.profile_typed().await?;
363 /// println!("User: {} ({})", profile.user_name, profile.email);
364 /// # Ok(())
365 /// # }
366 /// ```
367 pub async fn profile_typed(&self) -> KiteResult<UserProfile> {
368 let resp = self
369 .send_request_with_rate_limiting_and_retry(KiteEndpoint::Profile, &[], None, None)
370 .await?;
371 let json_response = self.raise_or_return_json_typed(resp).await?;
372
373 // Extract the data field from response
374 let data = json_response["data"].clone();
375 self.parse_response(data)
376 }
377
378 /// Invalidates access token with typed response
379 ///
380 /// Returns strongly typed logout response instead of JsonValue.
381 /// This is the preferred method for new applications.
382 ///
383 /// # Arguments
384 ///
385 /// * `access_token` - The access token to invalidate
386 ///
387 /// # Returns
388 ///
389 /// A `KiteResult<bool>` indicating success (true) or failure
390 ///
391 /// # Example
392 ///
393 /// ```rust,no_run
394 /// use kiteconnect_async_wasm::connect::KiteConnect;
395 ///
396 /// # #[tokio::main]
397 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
398 /// let client = KiteConnect::new("api_key", "access_token");
399 ///
400 /// let success = client.invalidate_access_token_typed("access_token").await?;
401 /// println!("Session invalidated: {}", success);
402 /// # Ok(())
403 /// # }
404 /// ```
405 pub async fn invalidate_access_token_typed(&self, access_token: &str) -> KiteResult<bool> {
406 let json_response = self
407 .invalidate_access_token(access_token)
408 .await
409 .map_err(crate::models::common::KiteError::Legacy)?;
410
411 // According to the API docs, response format is { "status": "success", "data": true }
412 match json_response["data"].as_bool() {
413 Some(success) => Ok(success),
414 None => {
415 // Fallback: if data field is not boolean, parse the whole response
416 Ok(json_response["status"].as_str() == Some("success"))
417 }
418 }
419 }
420}