use bcrypt::BcryptResult;
use jsonwebtoken::{
decode, encode, errors::Result as JWTResult, get_current_timestamp, Algorithm, DecodingKey,
EncodingKey, Header, TokenData, Validation,
};
use serde::{Deserialize, Serialize};
const JWT_ALGORITHM: Algorithm = Algorithm::HS512;
#[derive(Debug, Serialize, Deserialize)]
pub struct UserClaims {
pub pid: String,
exp: usize,
}
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) -> JWTResult<String> {
#[allow(clippy::cast_possible_truncation)]
let exp = (get_current_timestamp() + expiration) as usize;
let claims = UserClaims { pid, exp };
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,
)
}
}
pub fn hash_password(pass: &str) -> BcryptResult<String> {
bcrypt::hash(pass.as_bytes(), bcrypt::DEFAULT_COST)
}
pub fn verify_password(pass: &str, hashed_password: &str) -> BcryptResult<bool> {
bcrypt::verify(pass.as_bytes(), hashed_password)
}
#[cfg(test)]
mod tests {
use insta::{assert_debug_snapshot, with_settings};
use rstest::rstest;
use super::*;
#[rstest]
#[case("valid token", 60)]
#[case("token expired", 1)]
#[tokio::test]
async fn can_generate_token(#[case] test_name: &str, #[case] expiration: u64) {
let jwt = JWT::new("PqRwLF2rhHe8J22oBeHy");
let token = jwt.generate_token(&expiration, "pid".to_string()).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));
});
}
}