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}