use crate::config::{SaTokenConfig, TokenStyle};
use crate::error::{SaTokenError, SaTokenResult};
use crate::token::TokenValue;
use crate::token::csprng::{fill_bytes, random_hex, random_tik};
use crate::token::jwt::{JwtAlgorithm, JwtClaims, JwtManager};
use chrono::Utc;
use sha2::{Digest, Sha256};
use uuid::Uuid;
pub struct TokenGenerator;
impl TokenGenerator {
pub fn generate_with_login_id(
config: &SaTokenConfig,
login_id: &str,
) -> SaTokenResult<TokenValue> {
match config.token_style {
TokenStyle::Uuid => Ok(Self::generate_uuid()),
TokenStyle::SimpleUuid => Ok(Self::generate_simple_uuid()),
TokenStyle::Random32 => Self::generate_random_csprng(32),
TokenStyle::Random64 => Self::generate_random_csprng(64),
TokenStyle::Random128 => Self::generate_random_csprng(128),
TokenStyle::Jwt => Self::generate_jwt(config, login_id),
TokenStyle::Hash => Self::generate_hash(login_id),
TokenStyle::Timestamp => Self::generate_timestamp(),
TokenStyle::Tik => Self::generate_tik(),
}
}
pub fn generate_with_login_id_and_extra(
config: &SaTokenConfig,
login_id: &str,
extra_data: &serde_json::Value,
) -> SaTokenResult<TokenValue> {
match config.token_style {
TokenStyle::Jwt => Self::generate_jwt_with_extra(config, login_id, extra_data),
_ => Self::generate_with_login_id(config, login_id),
}
}
pub fn generate(config: &SaTokenConfig) -> SaTokenResult<TokenValue> {
Self::generate_with_login_id(config, "")
}
pub fn generate_uuid() -> TokenValue {
TokenValue::new(Uuid::new_v4().to_string())
}
pub fn generate_simple_uuid() -> TokenValue {
TokenValue::new(Uuid::new_v4().simple().to_string())
}
pub fn generate_random_csprng(length: usize) -> SaTokenResult<TokenValue> {
Ok(TokenValue::new(random_hex(length)?))
}
pub fn generate_random(length: usize) -> SaTokenResult<TokenValue> {
Self::generate_random_csprng(length)
}
pub fn generate_jwt(config: &SaTokenConfig, login_id: &str) -> SaTokenResult<TokenValue> {
let secret = require_jwt_secret(config)?;
let effective_login_id = if login_id.is_empty() {
Utc::now().timestamp_millis().to_string()
} else {
login_id.to_string()
};
let algorithm = config
.jwt_algorithm
.as_ref()
.and_then(|alg| Self::parse_jwt_algorithm(alg))
.unwrap_or(JwtAlgorithm::HS256);
let mut jwt_manager = JwtManager::with_algorithm(secret, algorithm);
if let Some(ref issuer) = config.jwt_issuer {
jwt_manager = jwt_manager.set_issuer(issuer);
}
if let Some(ref audience) = config.jwt_audience {
jwt_manager = jwt_manager.set_audience(audience);
}
let mut claims = JwtClaims::new(effective_login_id);
if config.timeout > 0 {
claims.set_expiration(config.timeout);
}
match jwt_manager.generate(&claims) {
Ok(token) => Ok(TokenValue::new(token)),
Err(e) if config.jwt_fallback_on_error => {
tracing::warn!(error = %e, "JWT generation failed, falling back to UUID");
Ok(Self::generate_uuid())
}
Err(e) => Err(SaTokenError::ConfigError(format!(
"JWT generation failed: {e}"
))),
}
}
pub fn generate_jwt_with_extra(
config: &SaTokenConfig,
login_id: &str,
extra_data: &serde_json::Value,
) -> SaTokenResult<TokenValue> {
let secret = require_jwt_secret(config)?;
let effective_login_id = if login_id.is_empty() {
Utc::now().timestamp_millis().to_string()
} else {
login_id.to_string()
};
let algorithm = config
.jwt_algorithm
.as_ref()
.and_then(|alg| Self::parse_jwt_algorithm(alg))
.unwrap_or(JwtAlgorithm::HS256);
let mut jwt_manager = JwtManager::with_algorithm(secret, algorithm);
if let Some(ref issuer) = config.jwt_issuer {
jwt_manager = jwt_manager.set_issuer(issuer);
}
if let Some(ref audience) = config.jwt_audience {
jwt_manager = jwt_manager.set_audience(audience);
}
let mut claims = JwtClaims::new(effective_login_id);
if config.timeout > 0 {
claims.set_expiration(config.timeout);
}
match extra_data {
serde_json::Value::Object(map) => {
for (key, value) in map {
claims.add_claim(key.clone(), value.clone());
}
}
serde_json::Value::Null => {}
other => {
claims.add_claim("extra", other.clone());
}
}
match jwt_manager.generate(&claims) {
Ok(token) => Ok(TokenValue::new(token)),
Err(e) if config.jwt_fallback_on_error => {
tracing::warn!(error = %e, "JWT generation with extra failed, falling back to UUID");
Ok(Self::generate_uuid())
}
Err(e) => Err(SaTokenError::ConfigError(format!(
"JWT generation failed: {e}"
))),
}
}
pub fn generate_hash(login_id: &str) -> SaTokenResult<TokenValue> {
let login_id_value = if login_id.is_empty() {
Utc::now().timestamp_millis().to_string()
} else {
login_id.to_string()
};
let mut salt = [0u8; 16];
fill_bytes(&mut salt)?;
let data = format!(
"{}{}{}",
login_id_value,
Utc::now().timestamp_millis(),
hex::encode(salt)
);
let mut hasher = Sha256::new();
hasher.update(data.as_bytes());
Ok(TokenValue::new(hex::encode(hasher.finalize())))
}
pub fn generate_timestamp() -> SaTokenResult<TokenValue> {
let timestamp = Utc::now().timestamp_millis();
let suffix = random_hex(16)?;
Ok(TokenValue::new(format!("{timestamp}_{suffix}")))
}
pub fn generate_tik() -> SaTokenResult<TokenValue> {
Ok(TokenValue::new(random_tik(8)?))
}
fn parse_jwt_algorithm(alg: &str) -> Option<JwtAlgorithm> {
match alg.to_uppercase().as_str() {
"HS256" => Some(JwtAlgorithm::HS256),
"HS384" => Some(JwtAlgorithm::HS384),
"HS512" => Some(JwtAlgorithm::HS512),
"RS256" => Some(JwtAlgorithm::RS256),
"RS384" => Some(JwtAlgorithm::RS384),
"RS512" => Some(JwtAlgorithm::RS512),
"ES256" => Some(JwtAlgorithm::ES256),
"ES384" => Some(JwtAlgorithm::ES384),
_ => None,
}
}
}
impl std::fmt::Debug for TokenGenerator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("TokenGenerator { .. }")
}
}
fn require_jwt_secret(config: &SaTokenConfig) -> SaTokenResult<&str> {
match config.jwt_secret_key.as_deref() {
Some(s) if !s.trim().is_empty() => Ok(s),
_ => Err(SaTokenError::ConfigError(
"jwt_secret_key is required when token_style=Jwt".into(),
)),
}
}
pub async fn generate_unique<C, F, Fut>(
max_try_times: i32,
mut create: C,
mut occupied: F,
) -> SaTokenResult<TokenValue>
where
C: FnMut() -> SaTokenResult<TokenValue>,
F: FnMut(&str) -> Fut,
Fut: Future<Output = SaTokenResult<bool>>,
{
if max_try_times <= 0 {
return create();
}
let mut last = create()?;
for _ in 0..max_try_times {
if !occupied(last.as_str()).await? {
return Ok(last);
}
last = create()?;
}
if !occupied(last.as_str()).await? {
return Ok(last);
}
Err(SaTokenError::ConfigError(format!(
"failed to generate a unique token after {max_try_times} attempts"
)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{SaTokenConfig, TokenStyle};
use crate::token::jwt::JwtManager;
fn jwt_config() -> SaTokenConfig {
SaTokenConfig {
token_style: TokenStyle::Jwt,
jwt_secret_key: Some("test-secret-key-for-jwt".to_string()),
timeout: 3600,
..SaTokenConfig::default()
}
}
#[test]
fn test_generate_jwt_with_extra_object() {
let config = jwt_config();
let extra = serde_json::json!({
"role": "admin",
"tenant_id": 42,
"permissions": ["read", "write"]
});
let token = TokenGenerator::generate_jwt_with_extra(&config, "user_123", &extra).unwrap();
assert!(!token.as_str().is_empty());
let jwt_manager = JwtManager::new("test-secret-key-for-jwt");
let claims = jwt_manager.validate(token.as_str()).unwrap();
assert_eq!(claims.login_id, "user_123");
assert_eq!(claims.get_claim("role"), Some(&serde_json::json!("admin")));
assert_eq!(claims.get_claim("tenant_id"), Some(&serde_json::json!(42)));
assert_eq!(
claims.get_claim("permissions"),
Some(&serde_json::json!(["read", "write"]))
);
}
#[test]
fn test_generate_jwt_with_extra_non_object() {
let config = jwt_config();
let extra = serde_json::json!("simple_string_value");
let token = TokenGenerator::generate_jwt_with_extra(&config, "user_456", &extra).unwrap();
let jwt_manager = JwtManager::new("test-secret-key-for-jwt");
let claims = jwt_manager.validate(token.as_str()).unwrap();
assert_eq!(claims.login_id, "user_456");
assert_eq!(
claims.get_claim("extra"),
Some(&serde_json::json!("simple_string_value"))
);
}
#[test]
fn test_generate_jwt_with_extra_null() {
let config = jwt_config();
let extra = serde_json::Value::Null;
let token = TokenGenerator::generate_jwt_with_extra(&config, "user_789", &extra).unwrap();
let jwt_manager = JwtManager::new("test-secret-key-for-jwt");
let claims = jwt_manager.validate(token.as_str()).unwrap();
assert_eq!(claims.login_id, "user_789");
assert!(claims.extra.is_empty());
}
#[test]
fn test_generate_with_login_id_and_extra_jwt_style() {
let config = jwt_config();
let extra = serde_json::json!({"key": "value"});
let token =
TokenGenerator::generate_with_login_id_and_extra(&config, "user_jwt", &extra).unwrap();
assert!(token.as_str().contains('.'));
let jwt_manager = JwtManager::new("test-secret-key-for-jwt");
let claims = jwt_manager.validate(token.as_str()).unwrap();
assert_eq!(claims.get_claim("key"), Some(&serde_json::json!("value")));
}
#[test]
fn test_generate_with_login_id_and_extra_non_jwt_style() {
let config = SaTokenConfig {
token_style: TokenStyle::Uuid,
..SaTokenConfig::default()
};
let extra = serde_json::json!({"key": "value"});
let token =
TokenGenerator::generate_with_login_id_and_extra(&config, "user_uuid", &extra).unwrap();
assert!(!token.as_str().is_empty());
assert!(!token.as_str().contains('.'));
}
#[test]
fn test_random_32_length() {
let config = SaTokenConfig {
token_style: TokenStyle::Random32,
..SaTokenConfig::default()
};
let token = TokenGenerator::generate_with_login_id(&config, "user_random").unwrap();
assert!(!token.as_str().is_empty());
assert_eq!(token.as_str().len(), 32);
}
#[test]
fn test_random_64_length() {
let config = SaTokenConfig {
token_style: TokenStyle::Random64,
..SaTokenConfig::default()
};
let token = TokenGenerator::generate_with_login_id(&config, "user_random").unwrap();
assert!(!token.as_str().is_empty());
assert_eq!(token.as_str().len(), 64);
}
#[test]
fn test_random_128_length() {
let config = SaTokenConfig {
token_style: TokenStyle::Random128,
..SaTokenConfig::default()
};
let token = TokenGenerator::generate_with_login_id(&config, "user_random").unwrap();
assert!(!token.as_str().is_empty());
assert_eq!(token.as_str().len(), 128);
}
}