1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
5use hmac::{Hmac, Mac};
6use rand::Rng;
7use serde::{Deserialize, Serialize};
8use sha2::Sha256;
9use std::collections::HashMap;
10use std::env;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13type HmacSha256 = Hmac<Sha256>;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct TokenSubject {
18 pub id: String,
19 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
20 pub subject_type: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
22 pub tenant_id: Option<String>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub organization_id: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub teams: Option<Vec<String>>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub groups: Option<Vec<String>>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub roles: Option<Vec<String>>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub permissions: Option<Vec<String>>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub metadata: Option<HashMap<String, serde_json::Value>>,
35}
36
37#[derive(Debug, Clone, Default)]
39pub struct TokenOptions {
40 pub expires_in: Option<u64>, pub issuer: Option<String>,
42 pub audience: Option<Vec<String>>,
43 pub not_before: Option<u64>, pub secret_key: Option<String>, }
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct TokenPayload {
50 pub sub: String,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub iss: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub aud: Option<Vec<String>>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub exp: Option<u64>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub iat: Option<u64>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub nbf: Option<u64>,
61 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
62 pub subject_type: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub tenant_id: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub organization_id: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub teams: Option<Vec<String>>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub groups: Option<Vec<String>>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub roles: Option<Vec<String>>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub permissions: Option<Vec<String>>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub metadata: Option<HashMap<String, serde_json::Value>>,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub sdk_version: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub application_id: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub session_id: Option<String>,
83}
84
85#[derive(Debug)]
87pub enum AuthError {
88 MissingSecretKey,
89 InvalidTokenFormat,
90 InvalidSignature,
91 InvalidPayload(String),
92 SerializationError(String),
93}
94
95impl std::fmt::Display for AuthError {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 match self {
98 AuthError::MissingSecretKey => write!(
99 f,
100 "ZEAL_SECRET_KEY is required for token generation. Set it as an environment variable or pass it in options"
101 ),
102 AuthError::InvalidTokenFormat => write!(f, "Invalid token format"),
103 AuthError::InvalidSignature => write!(f, "Invalid token signature"),
104 AuthError::InvalidPayload(msg) => write!(f, "Invalid token payload: {}", msg),
105 AuthError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
106 }
107 }
108}
109
110impl std::error::Error for AuthError {}
111
112pub fn generate_auth_token(
116 subject: &TokenSubject,
117 options: Option<TokenOptions>,
118) -> Result<String, AuthError> {
119 let options = options.unwrap_or_default();
120
121 let secret_key = options
123 .secret_key
124 .or_else(|| env::var("ZEAL_SECRET_KEY").ok())
125 .ok_or(AuthError::MissingSecretKey)?;
126
127 let now = SystemTime::now()
128 .duration_since(UNIX_EPOCH)
129 .unwrap()
130 .as_secs();
131
132 let session_id: String = rand::thread_rng()
134 .sample_iter(&rand::distributions::Alphanumeric)
135 .take(16)
136 .map(char::from)
137 .collect();
138
139 let mut payload = TokenPayload {
140 sub: subject.id.clone(),
141 iat: Some(now),
142 subject_type: subject.subject_type.clone(),
143 tenant_id: subject.tenant_id.clone(),
144 organization_id: subject.organization_id.clone(),
145 teams: subject.teams.clone(),
146 groups: subject.groups.clone(),
147 roles: subject.roles.clone(),
148 permissions: subject.permissions.clone(),
149 metadata: subject.metadata.clone(),
150 sdk_version: Some("1.0.0".to_string()),
151 application_id: Some("zeal-rust-sdk".to_string()),
152 session_id: Some(session_id),
153 iss: None,
154 aud: None,
155 exp: None,
156 nbf: None,
157 };
158
159 if let Some(expires_in) = options.expires_in {
161 payload.exp = Some(now + expires_in);
162 }
163 if let Some(issuer) = options.issuer {
164 payload.iss = Some(issuer);
165 }
166 if let Some(audience) = options.audience {
167 payload.aud = Some(audience);
168 }
169 if let Some(not_before) = options.not_before {
170 payload.nbf = Some(not_before);
171 }
172
173 let payload_json = serde_json::to_string(&payload)
175 .map_err(|e| AuthError::SerializationError(e.to_string()))?;
176 let encoded_payload = URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
177
178 let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes())
180 .map_err(|e| AuthError::SerializationError(e.to_string()))?;
181 mac.update(encoded_payload.as_bytes());
182 let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
183
184 Ok(format!("{}.{}", encoded_payload, signature))
186}
187
188pub fn verify_and_parse_token(
191 token: &str,
192 secret_key: Option<String>,
193) -> Result<TokenPayload, AuthError> {
194 let key = secret_key
195 .or_else(|| env::var("ZEAL_SECRET_KEY").ok())
196 .ok_or(AuthError::MissingSecretKey)?;
197
198 let parts: Vec<&str> = token.split('.').collect();
199 if parts.len() != 2 {
200 return Err(AuthError::InvalidTokenFormat);
201 }
202
203 let encoded_payload = parts[0];
204 let signature = parts[1];
205
206 let mut mac = HmacSha256::new_from_slice(key.as_bytes())
208 .map_err(|e| AuthError::SerializationError(e.to_string()))?;
209 mac.update(encoded_payload.as_bytes());
210 let expected_signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
211
212 if signature != expected_signature {
213 return Err(AuthError::InvalidSignature);
214 }
215
216 let payload_bytes = URL_SAFE_NO_PAD
218 .decode(encoded_payload)
219 .map_err(|e| AuthError::InvalidPayload(e.to_string()))?;
220 let payload: TokenPayload = serde_json::from_slice(&payload_bytes)
221 .map_err(|e| AuthError::InvalidPayload(e.to_string()))?;
222
223 Ok(payload)
224}
225
226pub fn parse_token_unsafe(token: &str) -> Result<TokenPayload, AuthError> {
229 let parts: Vec<&str> = token.split('.').collect();
230 if parts.len() != 2 {
231 return Err(AuthError::InvalidTokenFormat);
232 }
233
234 let encoded_payload = parts[0];
235
236 let payload_bytes = URL_SAFE_NO_PAD
237 .decode(encoded_payload)
238 .map_err(|e| AuthError::InvalidPayload(e.to_string()))?;
239 let payload: TokenPayload = serde_json::from_slice(&payload_bytes)
240 .map_err(|e| AuthError::InvalidPayload(e.to_string()))?;
241
242 Ok(payload)
243}
244
245pub fn create_service_token(
248 service_id: &str,
249 tenant_id: &str,
250 permissions: Vec<String>,
251 options: Option<TokenOptions>,
252) -> Result<String, AuthError> {
253 let mut metadata = HashMap::new();
254 metadata.insert("service".to_string(), serde_json::json!(true));
255 metadata.insert(
256 "created_at".to_string(),
257 serde_json::json!(chrono::Utc::now().to_rfc3339()),
258 );
259
260 generate_auth_token(
261 &TokenSubject {
262 id: service_id.to_string(),
263 subject_type: Some("service".to_string()),
264 tenant_id: Some(tenant_id.to_string()),
265 permissions: Some(permissions),
266 metadata: Some(metadata),
267 organization_id: None,
268 teams: None,
269 groups: None,
270 roles: None,
271 },
272 options,
273 )
274}
275
276pub fn create_user_token(
279 user_id: &str,
280 tenant_id: &str,
281 roles: Vec<String>,
282 options: Option<TokenOptions>,
283) -> Result<String, AuthError> {
284 let mut metadata = HashMap::new();
285 metadata.insert(
286 "created_at".to_string(),
287 serde_json::json!(chrono::Utc::now().to_rfc3339()),
288 );
289
290 generate_auth_token(
291 &TokenSubject {
292 id: user_id.to_string(),
293 subject_type: Some("user".to_string()),
294 tenant_id: Some(tenant_id.to_string()),
295 roles: Some(roles),
296 metadata: Some(metadata),
297 organization_id: None,
298 teams: None,
299 groups: None,
300 permissions: None,
301 },
302 options,
303 )
304}
305
306pub fn create_api_key_token(
309 api_key_id: &str,
310 tenant_id: &str,
311 permissions: Vec<String>,
312 options: Option<TokenOptions>,
313) -> Result<String, AuthError> {
314 let mut metadata = HashMap::new();
315 metadata.insert("api_key".to_string(), serde_json::json!(true));
316 metadata.insert(
317 "created_at".to_string(),
318 serde_json::json!(chrono::Utc::now().to_rfc3339()),
319 );
320
321 generate_auth_token(
322 &TokenSubject {
323 id: api_key_id.to_string(),
324 subject_type: Some("api_key".to_string()),
325 tenant_id: Some(tenant_id.to_string()),
326 permissions: Some(permissions),
327 metadata: Some(metadata),
328 organization_id: None,
329 teams: None,
330 groups: None,
331 roles: None,
332 },
333 options,
334 )
335}
336
337pub fn is_token_valid(token: &str, secret_key: Option<String>) -> bool {
340 match verify_and_parse_token(token, secret_key) {
341 Ok(payload) => {
342 let now = SystemTime::now()
343 .duration_since(UNIX_EPOCH)
344 .unwrap()
345 .as_secs();
346
347 if let Some(exp) = payload.exp {
349 if exp < now {
350 return false;
351 }
352 }
353
354 if let Some(nbf) = payload.nbf {
356 if nbf > now {
357 return false;
358 }
359 }
360
361 true
362 }
363 Err(_) => false,
364 }
365}