use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use hmac::{Hmac, Mac};
use rand::Rng;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::collections::HashMap;
use std::env;
use std::time::{SystemTime, UNIX_EPOCH};
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenSubject {
pub id: String,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub subject_type: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub tenant_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub organization_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub teams: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub groups: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Default)]
pub struct TokenOptions {
pub expires_in: Option<u64>, pub issuer: Option<String>,
pub audience: Option<Vec<String>>,
pub not_before: Option<u64>, pub secret_key: Option<String>, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenPayload {
pub sub: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aud: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exp: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iat: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nbf: Option<u64>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub subject_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tenant_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub organization_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub teams: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub groups: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sdk_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub application_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
}
#[derive(Debug)]
pub enum AuthError {
MissingSecretKey,
InvalidTokenFormat,
InvalidSignature,
InvalidPayload(String),
SerializationError(String),
}
impl std::fmt::Display for AuthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthError::MissingSecretKey => write!(
f,
"ZEAL_SECRET_KEY is required for token generation. Set it as an environment variable or pass it in options"
),
AuthError::InvalidTokenFormat => write!(f, "Invalid token format"),
AuthError::InvalidSignature => write!(f, "Invalid token signature"),
AuthError::InvalidPayload(msg) => write!(f, "Invalid token payload: {}", msg),
AuthError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
}
}
}
impl std::error::Error for AuthError {}
pub fn generate_auth_token(
subject: &TokenSubject,
options: Option<TokenOptions>,
) -> Result<String, AuthError> {
let options = options.unwrap_or_default();
let secret_key = options
.secret_key
.or_else(|| env::var("ZEAL_SECRET_KEY").ok())
.ok_or(AuthError::MissingSecretKey)?;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let session_id: String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(16)
.map(char::from)
.collect();
let mut payload = TokenPayload {
sub: subject.id.clone(),
iat: Some(now),
subject_type: subject.subject_type.clone(),
tenant_id: subject.tenant_id.clone(),
organization_id: subject.organization_id.clone(),
teams: subject.teams.clone(),
groups: subject.groups.clone(),
roles: subject.roles.clone(),
permissions: subject.permissions.clone(),
metadata: subject.metadata.clone(),
sdk_version: Some("1.0.0".to_string()),
application_id: Some("zeal-rust-sdk".to_string()),
session_id: Some(session_id),
iss: None,
aud: None,
exp: None,
nbf: None,
};
if let Some(expires_in) = options.expires_in {
payload.exp = Some(now + expires_in);
}
if let Some(issuer) = options.issuer {
payload.iss = Some(issuer);
}
if let Some(audience) = options.audience {
payload.aud = Some(audience);
}
if let Some(not_before) = options.not_before {
payload.nbf = Some(not_before);
}
let payload_json = serde_json::to_string(&payload)
.map_err(|e| AuthError::SerializationError(e.to_string()))?;
let encoded_payload = URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes())
.map_err(|e| AuthError::SerializationError(e.to_string()))?;
mac.update(encoded_payload.as_bytes());
let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
Ok(format!("{}.{}", encoded_payload, signature))
}
pub fn verify_and_parse_token(
token: &str,
secret_key: Option<String>,
) -> Result<TokenPayload, AuthError> {
let key = secret_key
.or_else(|| env::var("ZEAL_SECRET_KEY").ok())
.ok_or(AuthError::MissingSecretKey)?;
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 2 {
return Err(AuthError::InvalidTokenFormat);
}
let encoded_payload = parts[0];
let signature = parts[1];
let mut mac = HmacSha256::new_from_slice(key.as_bytes())
.map_err(|e| AuthError::SerializationError(e.to_string()))?;
mac.update(encoded_payload.as_bytes());
let expected_signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
if signature != expected_signature {
return Err(AuthError::InvalidSignature);
}
let payload_bytes = URL_SAFE_NO_PAD
.decode(encoded_payload)
.map_err(|e| AuthError::InvalidPayload(e.to_string()))?;
let payload: TokenPayload = serde_json::from_slice(&payload_bytes)
.map_err(|e| AuthError::InvalidPayload(e.to_string()))?;
Ok(payload)
}
pub fn parse_token_unsafe(token: &str) -> Result<TokenPayload, AuthError> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 2 {
return Err(AuthError::InvalidTokenFormat);
}
let encoded_payload = parts[0];
let payload_bytes = URL_SAFE_NO_PAD
.decode(encoded_payload)
.map_err(|e| AuthError::InvalidPayload(e.to_string()))?;
let payload: TokenPayload = serde_json::from_slice(&payload_bytes)
.map_err(|e| AuthError::InvalidPayload(e.to_string()))?;
Ok(payload)
}
pub fn create_service_token(
service_id: &str,
tenant_id: &str,
permissions: Vec<String>,
options: Option<TokenOptions>,
) -> Result<String, AuthError> {
let mut metadata = HashMap::new();
metadata.insert("service".to_string(), serde_json::json!(true));
metadata.insert(
"created_at".to_string(),
serde_json::json!(chrono::Utc::now().to_rfc3339()),
);
generate_auth_token(
&TokenSubject {
id: service_id.to_string(),
subject_type: Some("service".to_string()),
tenant_id: Some(tenant_id.to_string()),
permissions: Some(permissions),
metadata: Some(metadata),
organization_id: None,
teams: None,
groups: None,
roles: None,
},
options,
)
}
pub fn create_user_token(
user_id: &str,
tenant_id: &str,
roles: Vec<String>,
options: Option<TokenOptions>,
) -> Result<String, AuthError> {
let mut metadata = HashMap::new();
metadata.insert(
"created_at".to_string(),
serde_json::json!(chrono::Utc::now().to_rfc3339()),
);
generate_auth_token(
&TokenSubject {
id: user_id.to_string(),
subject_type: Some("user".to_string()),
tenant_id: Some(tenant_id.to_string()),
roles: Some(roles),
metadata: Some(metadata),
organization_id: None,
teams: None,
groups: None,
permissions: None,
},
options,
)
}
pub fn create_api_key_token(
api_key_id: &str,
tenant_id: &str,
permissions: Vec<String>,
options: Option<TokenOptions>,
) -> Result<String, AuthError> {
let mut metadata = HashMap::new();
metadata.insert("api_key".to_string(), serde_json::json!(true));
metadata.insert(
"created_at".to_string(),
serde_json::json!(chrono::Utc::now().to_rfc3339()),
);
generate_auth_token(
&TokenSubject {
id: api_key_id.to_string(),
subject_type: Some("api_key".to_string()),
tenant_id: Some(tenant_id.to_string()),
permissions: Some(permissions),
metadata: Some(metadata),
organization_id: None,
teams: None,
groups: None,
roles: None,
},
options,
)
}
pub fn is_token_valid(token: &str, secret_key: Option<String>) -> bool {
match verify_and_parse_token(token, secret_key) {
Ok(payload) => {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
if let Some(exp) = payload.exp {
if exp < now {
return false;
}
}
if let Some(nbf) = payload.nbf {
if nbf > now {
return false;
}
}
true
}
Err(_) => false,
}
}