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}