pub mod paseto_generator;
#[cfg(feature = "jwt")]
pub mod jwt_generator;
pub mod refresh;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use crate::error::Error;
use crate::middleware::Claims;
pub trait TokenGenerator: Send + Sync + Clone {
fn generate_token(&self, claims: &Claims) -> Result<String, Error>;
fn generate_token_with_expiry(
&self,
claims: &Claims,
expires_in: Duration,
) -> Result<String, Error>;
fn default_lifetime(&self) -> Duration;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenPair {
pub access_token: String,
pub refresh_token: String,
pub token_type: String,
pub expires_in: i64,
pub refresh_expires_in: i64,
}
impl TokenPair {
pub fn new(
access_token: String,
refresh_token: String,
expires_in: i64,
refresh_expires_in: i64,
) -> Self {
Self {
access_token,
refresh_token,
token_type: "Bearer".to_string(),
expires_in,
refresh_expires_in,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ClaimsBuilder {
sub: Option<String>,
email: Option<String>,
username: Option<String>,
roles: Vec<String>,
perms: Vec<String>,
iss: Option<String>,
aud: Option<String>,
custom: HashMap<String, serde_json::Value>,
}
impl ClaimsBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn subject(mut self, sub: impl Into<String>) -> Self {
self.sub = Some(sub.into());
self
}
pub fn user(mut self, user_id: impl Into<String>) -> Self {
self.sub = Some(format!("user:{}", user_id.into()));
self
}
pub fn client(mut self, client_id: impl Into<String>) -> Self {
self.sub = Some(format!("client:{}", client_id.into()));
self
}
pub fn email(mut self, email: impl Into<String>) -> Self {
self.email = Some(email.into());
self
}
pub fn username(mut self, username: impl Into<String>) -> Self {
self.username = Some(username.into());
self
}
pub fn role(mut self, role: impl Into<String>) -> Self {
self.roles.push(role.into());
self
}
pub fn roles(mut self, roles: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.roles.extend(roles.into_iter().map(Into::into));
self
}
pub fn permission(mut self, perm: impl Into<String>) -> Self {
self.perms.push(perm.into());
self
}
pub fn permissions(mut self, perms: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.perms.extend(perms.into_iter().map(Into::into));
self
}
pub fn issuer(mut self, iss: impl Into<String>) -> Self {
self.iss = Some(iss.into());
self
}
pub fn audience(mut self, aud: impl Into<String>) -> Self {
self.aud = Some(aud.into());
self
}
pub fn custom_claim(
mut self,
key: impl Into<String>,
value: impl Into<serde_json::Value>,
) -> Self {
self.custom.insert(key.into(), value.into());
self
}
pub fn custom_claims(
mut self,
claims: impl IntoIterator<Item = (impl Into<String>, impl Into<serde_json::Value>)>,
) -> Self {
for (key, value) in claims {
self.custom.insert(key.into(), value.into());
}
self
}
pub fn build(self) -> Result<Claims, Error> {
let sub = self
.sub
.ok_or_else(|| Error::ValidationError("Subject (sub) is required".to_string()))?;
Ok(Claims {
sub,
email: self.email,
username: self.username,
roles: self.roles,
perms: self.perms,
exp: 0, iat: None, jti: None, iss: self.iss,
aud: self.aud,
custom: self.custom,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_claims_builder_user() {
let claims = ClaimsBuilder::new()
.user("123")
.email("test@example.com")
.role("user")
.role("admin")
.permission("read:docs")
.build()
.unwrap();
assert_eq!(claims.sub, "user:123");
assert_eq!(claims.email, Some("test@example.com".to_string()));
assert_eq!(claims.roles, vec!["user", "admin"]);
assert_eq!(claims.perms, vec!["read:docs"]);
}
#[test]
fn test_claims_builder_client() {
let claims = ClaimsBuilder::new()
.client("api-client-abc")
.roles(["service"])
.build()
.unwrap();
assert_eq!(claims.sub, "client:api-client-abc");
assert_eq!(claims.roles, vec!["service"]);
}
#[test]
fn test_claims_builder_custom_claims() {
let claims = ClaimsBuilder::new()
.user("123")
.custom_claim("tenant_id", serde_json::json!("org-42"))
.custom_claim("level", serde_json::json!(5))
.custom_claims([
("region".to_string(), serde_json::json!("us-east-1")),
("beta".to_string(), serde_json::json!(true)),
])
.build()
.unwrap();
assert_eq!(
claims.custom_claim_as::<String>("tenant_id"),
Some("org-42".to_string())
);
assert_eq!(claims.custom_claim_as::<i64>("level"), Some(5));
assert_eq!(
claims.custom_claim_as::<String>("region"),
Some("us-east-1".to_string())
);
assert_eq!(claims.custom_claim_as::<bool>("beta"), Some(true));
}
#[test]
fn test_claims_builder_missing_subject() {
let result = ClaimsBuilder::new().email("test@example.com").build();
assert!(result.is_err());
}
#[test]
fn test_token_pair_creation() {
let pair = TokenPair::new(
"access_token".to_string(),
"refresh_token".to_string(),
900,
604800,
);
assert_eq!(pair.access_token, "access_token");
assert_eq!(pair.refresh_token, "refresh_token");
assert_eq!(pair.token_type, "Bearer");
assert_eq!(pair.expires_in, 900);
assert_eq!(pair.refresh_expires_in, 604800);
}
}