noah_sdk/
auth.rs

1//! Authentication module for JWT signing and API key management
2//!
3//! This module provides authentication functionality for the Noah SDK.
4//! It supports both API key authentication and JWT signing.
5//!
6//! # Authentication Methods
7//!
8//! ## API Key Authentication
9//!
10//! Simple authentication using an API key:
11//!
12//! ```no_run
13//! use noah_sdk::AuthConfig;
14//!
15//! let auth = AuthConfig::with_api_key("your-api-key".to_string());
16//! ```
17//!
18//! ## JWT Signing
19//!
20//! More secure authentication using JWT signing with a secret key:
21//!
22//! ```no_run
23//! use noah_sdk::AuthConfig;
24//!
25//! let auth = AuthConfig::with_secret_key("your-secret-key".to_string());
26//! ```
27//!
28//! ## Both Methods
29//!
30//! You can use both API key and JWT signing together:
31//!
32//! ```no_run
33//! use noah_sdk::AuthConfig;
34//!
35//! let auth = AuthConfig::with_both(
36//!     "your-api-key".to_string(),
37//!     "your-secret-key".to_string()
38//! );
39//! ```
40
41use crate::error::{NoahError, Result};
42use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
43use hmac::{Hmac, Mac};
44use serde::{Deserialize, Serialize};
45use sha2::Sha256;
46use std::time::{SystemTime, UNIX_EPOCH};
47
48type HmacSha256 = Hmac<Sha256>;
49
50/// Authentication configuration
51///
52/// Configures how the client authenticates with the Noah API.
53/// Supports API key authentication, JWT signing, or both.
54///
55/// # Examples
56///
57/// ```no_run
58/// use noah_sdk::AuthConfig;
59///
60/// // API key only
61/// let auth = AuthConfig::with_api_key("your-api-key".to_string());
62///
63/// // JWT signing only
64/// let auth = AuthConfig::with_secret_key("your-secret-key".to_string());
65///
66/// // Both methods
67/// let auth = AuthConfig::with_both(
68///     "your-api-key".to_string(),
69///     "your-secret-key".to_string()
70/// );
71/// ```
72#[derive(Debug, Clone)]
73pub struct AuthConfig {
74    /// API key for X-Api-Key header
75    pub api_key: Option<String>,
76    /// Secret key for JWT signing
77    pub secret_key: Option<String>,
78}
79
80impl AuthConfig {
81    /// Create new auth config with API key only
82    ///
83    /// This method uses simple API key authentication via the `X-Api-Key` header.
84    ///
85    /// # Arguments
86    ///
87    /// * `api_key` - Your Noah API key
88    ///
89    /// # Examples
90    ///
91    /// ```no_run
92    /// use noah_sdk::AuthConfig;
93    ///
94    /// let auth = AuthConfig::with_api_key("your-api-key".to_string());
95    /// ```
96    pub fn with_api_key(api_key: String) -> Self {
97        Self {
98            api_key: Some(api_key),
99            secret_key: None,
100        }
101    }
102
103    /// Create new auth config with JWT secret key only
104    ///
105    /// This method uses JWT signing for authentication. The client will automatically
106    /// generate and sign JWTs for each request using the `Api-Signature` header.
107    ///
108    /// # Arguments
109    ///
110    /// * `secret_key` - Your Noah secret key for JWT signing
111    ///
112    /// # Examples
113    ///
114    /// ```no_run
115    /// use noah_sdk::AuthConfig;
116    ///
117    /// let auth = AuthConfig::with_secret_key("your-secret-key".to_string());
118    /// ```
119    pub fn with_secret_key(secret_key: String) -> Self {
120        Self {
121            api_key: None,
122            secret_key: Some(secret_key),
123        }
124    }
125
126    /// Create new auth config with both API key and secret key
127    ///
128    /// This method uses both API key authentication and JWT signing.
129    /// Both headers will be included in requests.
130    ///
131    /// # Arguments
132    ///
133    /// * `api_key` - Your Noah API key
134    /// * `secret_key` - Your Noah secret key for JWT signing
135    ///
136    /// # Examples
137    ///
138    /// ```no_run
139    /// use noah_sdk::AuthConfig;
140    ///
141    /// let auth = AuthConfig::with_both(
142    ///     "your-api-key".to_string(),
143    ///     "your-secret-key".to_string()
144    /// );
145    /// ```
146    pub fn with_both(api_key: String, secret_key: String) -> Self {
147        Self {
148            api_key: Some(api_key),
149            secret_key: Some(secret_key),
150        }
151    }
152}
153
154/// JWT payload structure
155#[derive(Debug, Serialize, Deserialize)]
156struct JwtPayload {
157    /// Issued at timestamp
158    iat: u64,
159    /// Expiration timestamp
160    exp: u64,
161    /// HTTP method
162    method: String,
163    /// Request path
164    path: String,
165    /// Request body hash (if applicable)
166    #[serde(skip_serializing_if = "Option::is_none")]
167    body_hash: Option<String>,
168}
169
170/// Generate JWT signature for Api-Signature header
171pub fn generate_jwt_signature(
172    secret_key: &str,
173    method: &str,
174    path: &str,
175    body: Option<&[u8]>,
176) -> Result<String> {
177    let now = SystemTime::now()
178        .duration_since(UNIX_EPOCH)
179        .map_err(|e| NoahError::JwtError(format!("Failed to get timestamp: {e}")))?;
180
181    let iat = now.as_secs();
182    let exp = iat + 300; // 5 minutes expiration
183
184    // Calculate body hash if body is provided
185    let body_hash = body.map(|b| {
186        use sha2::Digest;
187        let mut hasher = sha2::Sha256::new();
188        hasher.update(b);
189        format!("{:x}", hasher.finalize())
190    });
191
192    let payload = JwtPayload {
193        iat,
194        exp,
195        method: method.to_uppercase(),
196        path: path.to_string(),
197        body_hash,
198    };
199
200    // Create JWT header
201    let header = serde_json::json!({
202        "alg": "HS256",
203        "typ": "JWT"
204    });
205
206    // Encode header and payload
207    let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header)?);
208    let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload)?);
209    let message = format!("{header_b64}.{payload_b64}");
210
211    // Sign with HMAC-SHA256
212    let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes())
213        .map_err(|e| NoahError::JwtError(format!("Invalid secret key: {e}")))?;
214    mac.update(message.as_bytes());
215    let signature = mac.finalize();
216    let signature_b64 = URL_SAFE_NO_PAD.encode(signature.into_bytes());
217
218    Ok(format!("{message}.{signature_b64}"))
219}
220
221/// Add authentication headers to a request builder
222#[cfg(feature = "async")]
223pub fn add_auth_headers_async(
224    builder: reqwest::RequestBuilder,
225    auth_config: &AuthConfig,
226    method: &str,
227    path: &str,
228    body: Option<&[u8]>,
229) -> Result<reqwest::RequestBuilder> {
230    let mut builder = builder;
231
232    // Add API key if available
233    if let Some(ref api_key) = auth_config.api_key {
234        builder = builder.header("X-Api-Key", api_key);
235    }
236
237    // Add JWT signature if secret key is available
238    if let Some(ref secret_key) = auth_config.secret_key {
239        let signature = generate_jwt_signature(secret_key, method, path, body)?;
240        builder = builder.header("Api-Signature", signature);
241    }
242
243    Ok(builder)
244}
245
246/// Add authentication headers to a request builder (blocking)
247#[cfg(feature = "sync")]
248pub fn add_auth_headers_sync(
249    builder: reqwest::blocking::RequestBuilder,
250    auth_config: &AuthConfig,
251    method: &str,
252    path: &str,
253    body: Option<&[u8]>,
254) -> Result<reqwest::blocking::RequestBuilder> {
255    let mut builder = builder;
256
257    // Add API key if available
258    if let Some(ref api_key) = auth_config.api_key {
259        builder = builder.header("X-Api-Key", api_key);
260    }
261
262    // Add JWT signature if secret key is available
263    if let Some(ref secret_key) = auth_config.secret_key {
264        let signature = generate_jwt_signature(secret_key, method, path, body)?;
265        builder = builder.header("Api-Signature", signature);
266    }
267
268    Ok(builder)
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_jwt_signature_generation() {
277        let secret = "test-secret-key";
278        let method = "GET";
279        let path = "/balances";
280        let signature = generate_jwt_signature(secret, method, path, None).unwrap();
281        assert!(!signature.is_empty());
282        assert!(signature.contains('.'));
283    }
284}