use jsonwebtoken::{
decode, encode, errors::Result as JWTResult, get_current_timestamp, Algorithm, DecodingKey,
EncodingKey, Header, TokenData, Validation,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
const JWT_ALGORITHM: Algorithm = Algorithm::HS512;
#[derive(Debug, Serialize, Deserialize)]
pub struct UserClaims {
pub pid: String,
exp: u64,
claims: Option<Value>,
}
#[derive(Debug)]
pub struct JWT {
secret: String,
algorithm: Algorithm,
}
impl JWT {
#[must_use]
pub fn new(secret: &str) -> Self {
Self {
secret: secret.to_string(),
algorithm: JWT_ALGORITHM,
}
}
#[must_use]
pub fn algorithm(mut self, algorithm: Algorithm) -> Self {
self.algorithm = algorithm;
self
}
pub fn generate_token(
&self,
expiration: &u64,
pid: String,
claims: Option<Value>,
) -> JWTResult<String> {
let exp = get_current_timestamp().saturating_add(*expiration);
let claims = UserClaims { pid, exp, claims };
let token = encode(
&Header::new(self.algorithm),
&claims,
&EncodingKey::from_base64_secret(&self.secret)?,
)?;
Ok(token)
}
pub fn validate(&self, token: &str) -> JWTResult<TokenData<UserClaims>> {
let mut validate = Validation::new(self.algorithm);
validate.leeway = 0;
decode::<UserClaims>(
token,
&DecodingKey::from_base64_secret(&self.secret)?,
&validate,
)
}
}
#[cfg(test)]
mod tests {
use insta::{assert_debug_snapshot, with_settings};
use rstest::rstest;
use serde_json::json;
use super::*;
#[rstest]
#[case("valid token", 60, None)]
#[case("token expired", 1, None)]
#[case("valid token and custom claims", 60, Some(json!({})))]
#[tokio::test]
async fn can_generate_token(
#[case] test_name: &str,
#[case] expiration: u64,
#[case] claims: Option<Value>,
) {
let jwt = JWT::new("PqRwLF2rhHe8J22oBeHy");
let token = jwt
.generate_token(&expiration, "pid".to_string(), claims)
.unwrap();
std::thread::sleep(std::time::Duration::from_secs(3));
with_settings!({filters => vec![
(r"exp: (\d+),", "exp: EXP,")
]}, {
assert_debug_snapshot!(test_name, jwt.validate(&token));
});
}
}