use jsonwebtoken::{
decode, encode, errors::ErrorKind, errors::Result, DecodingKey, EncodingKey, Header, TokenData,
Validation,
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug)]
pub struct JwtManager {
base_secret: Vec<u8>,
access_ttl: Duration,
refresh_ttl: Duration,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum TokenType {
Access,
Refresh,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub exp: usize,
pub ip: String,
pub user_agent: String,
pub token_type: TokenType,
}
impl JwtManager {
pub fn new(base_secret: &str, access_ttl: Duration, refresh_ttl: Duration) -> Self {
JwtManager {
base_secret: base_secret.as_bytes().to_vec(),
access_ttl,
refresh_ttl,
}
}
fn generate_key(&self, ip: &[u8], user_agent: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(ip);
hasher.update(user_agent);
hasher.update(&self.base_secret);
let result = hasher.finalize();
let mut key = [0u8; 32];
key.copy_from_slice(&result);
key
}
fn now_ts() -> usize {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::from_secs(0))
.as_secs() as usize
}
pub fn create_token(
&self,
subject: &str,
ip: &str,
user_agent: &str,
token_type: TokenType,
) -> Result<String> {
let ttl = match token_type {
TokenType::Access => self.access_ttl,
TokenType::Refresh => self.refresh_ttl,
};
let exp = Self::now_ts() + ttl.as_secs() as usize;
let claims = Claims {
sub: subject.to_string(),
exp,
ip: ip.to_string(),
user_agent: user_agent.to_string(),
token_type,
};
let key = self.generate_key(ip.as_bytes(), user_agent.as_bytes());
encode(&Header::default(), &claims, &EncodingKey::from_secret(&key))
}
pub fn verify_token(
&self,
token: &str,
ip: &str,
user_agent: &str,
) -> Result<TokenData<Claims>> {
let key = self.generate_key(ip.as_bytes(), user_agent.as_bytes());
let data = decode::<Claims>(
token,
&DecodingKey::from_secret(&key),
&Validation::default(),
)?;
if data.claims.exp < Self::now_ts() {
return Err(jsonwebtoken::errors::Error::from(
ErrorKind::ExpiredSignature,
));
}
Ok(data)
}
pub fn refresh_access_token(
&self,
refresh_token: &str,
ip: &str,
user_agent: &str,
) -> Result<String> {
let data = self.verify_token(refresh_token, ip, user_agent)?;
if data.claims.token_type != TokenType::Refresh {
return Err(jsonwebtoken::errors::Error::from(ErrorKind::InvalidToken));
}
self.create_token(&data.claims.sub, ip, user_agent, TokenType::Access)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_access_and_refresh() {
let manager = JwtManager::new("secret", Duration::from_secs(2), Duration::from_secs(10));
let ip = "127.0.0.1";
let ua = "Mozilla/5.0";
let access_token = manager
.create_token("user1", ip, ua, TokenType::Access)
.unwrap();
let refresh_token = manager
.create_token("user1", ip, ua, TokenType::Refresh)
.unwrap();
let claims = manager.verify_token(&access_token, ip, ua).unwrap();
assert_eq!(claims.claims.token_type, TokenType::Access);
let claims = manager.verify_token(&refresh_token, ip, ua).unwrap();
assert_eq!(claims.claims.token_type, TokenType::Refresh);
let new_access = manager
.refresh_access_token(&refresh_token, ip, ua)
.unwrap();
let new_claims = manager.verify_token(&new_access, ip, ua).unwrap();
assert_eq!(new_claims.claims.token_type, TokenType::Access);
assert_eq!(new_claims.claims.sub, "user1");
}
}