use crate::{JwtConfig, JwtError, Result, StandardClaims, TokenPair};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, TokenData, Validation, decode, encode};
use serde::{Serialize, de::DeserializeOwned};
#[derive(Clone)]
pub struct JwtService {
config: JwtConfig,
encoding_key: EncodingKey,
decoding_key: DecodingKey,
validation: Validation,
}
impl JwtService {
pub fn new(config: JwtConfig) -> Result<Self> {
let encoding_key = config.encoding_key()?;
let decoding_key = config.decoding_key()?;
let validation = config.validation();
Ok(Self {
config,
encoding_key,
decoding_key,
validation,
})
}
pub fn sign<T: Serialize>(&self, claims: &T) -> Result<String> {
let header = Header::new(self.config.algorithm);
encode(&header, claims, &self.encoding_key).map_err(JwtError::from)
}
pub fn verify<T: DeserializeOwned>(&self, token: &str) -> Result<T> {
let token_data: TokenData<T> = decode(token, &self.decoding_key, &self.validation)
.map_err(|e| match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => JwtError::TokenExpired,
jsonwebtoken::errors::ErrorKind::InvalidSignature => JwtError::InvalidSignature,
_ => JwtError::EncodingError(e),
})?;
Ok(token_data.claims)
}
pub fn decode_unverified<T: DeserializeOwned>(&self, token: &str) -> Result<T> {
let token_data: TokenData<T> =
jsonwebtoken::dangerous::insecure_decode(token).map_err(JwtError::from)?;
Ok(token_data.claims)
}
pub fn generate_token_pair<T: Serialize + Clone>(&self, claims: &T) -> Result<TokenPair> {
let access_token = self.sign(claims)?;
let refresh_token = self.sign(claims)?;
Ok(TokenPair::new(
access_token,
refresh_token,
self.config.expires_in.as_secs() as i64,
self.config.refresh_expires_in.as_secs() as i64,
))
}
pub fn refresh_token<T: DeserializeOwned + Serialize + Clone>(
&self,
refresh_token: &str,
) -> Result<TokenPair> {
let claims: T = self.verify(refresh_token)?;
self.generate_token_pair(&claims)
}
pub fn sign_standard(
&self,
sub: String,
additional_claims: Option<serde_json::Value>,
) -> Result<String> {
let mut claims = StandardClaims::new()
.with_subject(sub)
.with_expiration(self.config.expires_in.as_secs() as i64);
if let Some(iss) = &self.config.issuer {
claims = claims.with_issuer(iss.clone());
}
if let Some(aud) = &self.config.audience {
claims = claims.with_audience(aud.clone());
}
if let Some(additional) = additional_claims {
let mut combined = serde_json::to_value(&claims)
.map_err(|e| JwtError::SerializationError(e.to_string()))?;
if let (Some(obj), serde_json::Value::Object(add_obj)) =
(combined.as_object_mut(), additional)
{
obj.extend(add_obj);
}
let token = self.sign(&combined)?;
return Ok(token);
}
self.sign(&claims)
}
pub fn config(&self) -> &JwtConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct TestClaims {
sub: String,
name: String,
exp: i64,
}
#[test]
fn test_sign_and_verify() {
let config = JwtConfig::new("test-secret".to_string());
let service = JwtService::new(config).unwrap();
let exp = (chrono::Utc::now() + chrono::Duration::hours(1)).timestamp();
let claims = TestClaims {
sub: "123".to_string(),
name: "Test User".to_string(),
exp,
};
let token = service.sign(&claims).unwrap();
let decoded: TestClaims = service.verify(&token).unwrap();
assert_eq!(decoded.sub, claims.sub);
assert_eq!(decoded.name, claims.name);
}
#[test]
fn test_invalid_signature() {
let config1 = JwtConfig::new("secret1".to_string());
let service1 = JwtService::new(config1).unwrap();
let config2 = JwtConfig::new("secret2".to_string());
let service2 = JwtService::new(config2).unwrap();
let exp = (chrono::Utc::now() + chrono::Duration::hours(1)).timestamp();
let claims = TestClaims {
sub: "123".to_string(),
name: "Test".to_string(),
exp,
};
let token = service1.sign(&claims).unwrap();
let result: Result<TestClaims> = service2.verify(&token);
assert!(matches!(result, Err(JwtError::InvalidSignature)));
}
#[test]
fn test_token_pair_generation() {
let config = JwtConfig::new("test-secret".to_string());
let service = JwtService::new(config).unwrap();
let exp = (chrono::Utc::now() + chrono::Duration::hours(1)).timestamp();
let claims = TestClaims {
sub: "123".to_string(),
name: "Test".to_string(),
exp,
};
let pair = service.generate_token_pair(&claims).unwrap();
assert!(!pair.access_token.is_empty());
assert!(!pair.refresh_token.is_empty());
assert_eq!(pair.token_type, "Bearer");
}
#[test]
fn test_decode_unverified() {
let config = JwtConfig::new("test-secret".to_string());
let service = JwtService::new(config).unwrap();
let exp = (chrono::Utc::now() + chrono::Duration::hours(1)).timestamp();
let claims = TestClaims {
sub: "123".to_string(),
name: "Test".to_string(),
exp,
};
let token = service.sign(&claims).unwrap();
let decoded: TestClaims = service.decode_unverified(&token).unwrap();
assert_eq!(decoded, claims);
}
}