1use crate::error::{NoahError, Result};
4use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
5use hmac::{Hmac, Mac};
6use serde::{Deserialize, Serialize};
7use sha2::Sha256;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10type HmacSha256 = Hmac<Sha256>;
11
12#[derive(Debug, Clone)]
14pub struct AuthConfig {
15 pub api_key: Option<String>,
17 pub secret_key: Option<String>,
19}
20
21impl AuthConfig {
22 pub fn with_api_key(api_key: String) -> Self {
24 Self {
25 api_key: Some(api_key),
26 secret_key: None,
27 }
28 }
29
30 pub fn with_secret_key(secret_key: String) -> Self {
32 Self {
33 api_key: None,
34 secret_key: Some(secret_key),
35 }
36 }
37
38 pub fn with_both(api_key: String, secret_key: String) -> Self {
40 Self {
41 api_key: Some(api_key),
42 secret_key: Some(secret_key),
43 }
44 }
45}
46
47#[derive(Debug, Serialize, Deserialize)]
49struct JwtPayload {
50 iat: u64,
52 exp: u64,
54 method: String,
56 path: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 body_hash: Option<String>,
61}
62
63pub fn generate_jwt_signature(
65 secret_key: &str,
66 method: &str,
67 path: &str,
68 body: Option<&[u8]>,
69) -> Result<String> {
70 let now = SystemTime::now()
71 .duration_since(UNIX_EPOCH)
72 .map_err(|e| NoahError::JwtError(format!("Failed to get timestamp: {e}")))?;
73
74 let iat = now.as_secs();
75 let exp = iat + 300; let body_hash = body.map(|b| {
79 use sha2::Digest;
80 let mut hasher = sha2::Sha256::new();
81 hasher.update(b);
82 format!("{:x}", hasher.finalize())
83 });
84
85 let payload = JwtPayload {
86 iat,
87 exp,
88 method: method.to_uppercase(),
89 path: path.to_string(),
90 body_hash,
91 };
92
93 let header = serde_json::json!({
95 "alg": "HS256",
96 "typ": "JWT"
97 });
98
99 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header)?);
101 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload)?);
102 let message = format!("{header_b64}.{payload_b64}");
103
104 let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes())
106 .map_err(|e| NoahError::JwtError(format!("Invalid secret key: {e}")))?;
107 mac.update(message.as_bytes());
108 let signature = mac.finalize();
109 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.into_bytes());
110
111 Ok(format!("{message}.{signature_b64}"))
112}
113
114#[cfg(feature = "async")]
116pub fn add_auth_headers_async(
117 builder: reqwest::RequestBuilder,
118 auth_config: &AuthConfig,
119 method: &str,
120 path: &str,
121 body: Option<&[u8]>,
122) -> Result<reqwest::RequestBuilder> {
123 let mut builder = builder;
124
125 if let Some(ref api_key) = auth_config.api_key {
127 builder = builder.header("X-Api-Key", api_key);
128 }
129
130 if let Some(ref secret_key) = auth_config.secret_key {
132 let signature = generate_jwt_signature(secret_key, method, path, body)?;
133 builder = builder.header("Api-Signature", signature);
134 }
135
136 Ok(builder)
137}
138
139#[cfg(feature = "sync")]
141pub fn add_auth_headers_sync(
142 builder: reqwest::blocking::RequestBuilder,
143 auth_config: &AuthConfig,
144 method: &str,
145 path: &str,
146 body: Option<&[u8]>,
147) -> Result<reqwest::blocking::RequestBuilder> {
148 let mut builder = builder;
149
150 if let Some(ref api_key) = auth_config.api_key {
152 builder = builder.header("X-Api-Key", api_key);
153 }
154
155 if let Some(ref secret_key) = auth_config.secret_key {
157 let signature = generate_jwt_signature(secret_key, method, path, body)?;
158 builder = builder.header("Api-Signature", signature);
159 }
160
161 Ok(builder)
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_jwt_signature_generation() {
170 let secret = "test-secret-key";
171 let method = "GET";
172 let path = "/balances";
173 let signature = generate_jwt_signature(secret, method, path, None).unwrap();
174 assert!(!signature.is_empty());
175 assert!(signature.contains('.'));
176 }
177}