use crate::config::JwtConfig;
use crate::error::LsError;
use crate::utils::current_epoch_seconds;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey};
use secrecy::ExposeSecret;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct JWT<T> {
pub payload: T,
pub sub: String,
pub exp: i64,
pub iat: i64,
}
#[derive(Clone)]
pub struct LsJwtService {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
token_validity_seconds: i64,
header_default: jsonwebtoken::Header,
validation_default: jsonwebtoken::Validation,
}
impl LsJwtService {
pub fn new(jwt_config: &JwtConfig) -> Result<LsJwtService, LsError> {
let alg = jwt_config.signature_algorithm;
if !matches!(alg, Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512) {
return Err(LsError::ConfigurationError {
message: format!(
"JWT signature_algorithm [{alg:?}] is not HMAC-family. \
LsJwtService only supports HS256/HS384/HS512 (symmetric \
secrets). For RS*/ES*/PS*/EdDSA, build EncodingKey and \
DecodingKey directly from PEM/DER and use jsonwebtoken \
primitives instead of this service."
),
});
}
let secret_bytes = jwt_config.secret.expose_secret().as_bytes();
if secret_bytes.is_empty() {
return Err(LsError::ConfigurationError { message: "JWT secret key cannot be empty".to_owned() });
}
let encoding_key = EncodingKey::from_secret(secret_bytes);
let decoding_key = DecodingKey::from_secret(secret_bytes);
let mut validation_default = jsonwebtoken::Validation::new(alg);
validation_default.leeway = 0;
Ok(LsJwtService {
encoding_key,
decoding_key,
token_validity_seconds: i64::from(jwt_config.token_validity_minutes) * 60,
header_default: jsonwebtoken::Header { alg, ..jsonwebtoken::Header::default() },
validation_default,
})
}
pub fn generate_from_payload<'a, T: serde::ser::Serialize>(
&self,
payload: &'a T,
) -> Result<(JWT<&'a T>, String), LsError> {
let issued_at = current_epoch_seconds();
let token = JWT { payload, sub: "".to_string(), exp: issued_at + self.token_validity_seconds, iat: issued_at };
self.generate_from_token(&token).map(|jwt| (token, jwt))
}
pub fn generate_from_token<T: serde::ser::Serialize>(&self, token: &JWT<T>) -> Result<String, LsError> {
let result = jsonwebtoken::encode(&self.header_default, &token, &self.encoding_key);
match result {
Ok(t) => Ok(t),
Err(e) => {
Err(LsError::GenerateTokenError { message: e.to_string() })
}
}
}
pub fn parse_payload<T: serde::de::DeserializeOwned>(&self, jwt_string: &str) -> Result<T, LsError> {
let result = self.parse_token(jwt_string);
match result {
Ok(t) => Ok(t.payload),
Err(e) => Err(e),
}
}
pub fn parse_token<T: serde::de::DeserializeOwned>(&self, jwt_string: &str) -> Result<JWT<T>, LsError> {
let result: Result<jsonwebtoken::TokenData<JWT<T>>, jsonwebtoken::errors::Error> =
jsonwebtoken::decode(jwt_string, &self.decoding_key, &self.validation_default);
match result {
Ok(t) => Ok(t.claims),
Err(e) => match *e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
Err(LsError::ExpiredTokenError { message: e.to_string() })
}
_ => Err(LsError::InvalidTokenError { message: e.to_string() }),
},
}
}
}
#[cfg(test)]
mod test {
use super::*;
use chrono::prelude::Local;
#[test]
fn service_should_be_send_and_sync() {
call_me_with_send_and_sync(new());
}
fn call_me_with_send_and_sync<T: Send + Sync>(_: T) {}
#[test]
fn should_create_jwt_string_from_token() {
let jwt = new();
let payload = MyTestClaym { id: Local::now().timestamp(), name: "Red".to_string() };
let token = super::JWT {
payload,
sub: "".to_string(),
exp: Local::now().timestamp() + 3600,
iat: Local::now().timestamp(),
};
let jwt_string = jwt.generate_from_token(&token).unwrap();
println!("Jwt string: [{jwt_string}]");
}
#[test]
fn should_create_jwt_string_from_payload() {
let jwt = new();
let payload = MyTestClaym { id: Local::now().timestamp(), name: "Red".to_string() };
let (jwt, jwt_string) = jwt.generate_from_payload(&payload).unwrap();
assert_eq!(payload.id, jwt.payload.id);
println!("Jwt string: [{jwt_string}]");
}
#[test]
fn should_parse_the_token() {
let jwt = new();
let payload = MyTestClaym { id: Local::now().timestamp(), name: "Red".to_string() };
let jwt_string = jwt.generate_from_payload(&payload).unwrap().1;
let parsed: MyTestClaym = jwt.parse_payload(&jwt_string).unwrap();
assert_eq!(payload.id, parsed.id);
assert_eq!(payload.name, parsed.name);
}
#[test]
fn should_parse_the_expiration_date() {
let jwt = new();
let payload = MyTestClaym { id: Local::now().timestamp(), name: "Red".to_string() };
let time_before = Local::now().timestamp();
let jwt_string = jwt.generate_from_payload(&payload).unwrap().1;
let time_after = Local::now().timestamp();
let token: super::JWT<MyTestClaym> = jwt.parse_token(&jwt_string).unwrap();
assert_eq!(payload.id, token.payload.id);
assert_eq!(&payload.name, &token.payload.name);
let issued_at = token.iat;
let expiration = token.exp;
let timeout = 60_i64 * 60;
assert!(issued_at >= time_before);
assert!(issued_at <= time_after);
assert_eq!(issued_at + timeout, expiration);
}
#[test]
fn should_fail_parsing_tampered_token() {
let jwt = new();
let payload = MyTestClaym { id: Local::now().timestamp(), name: "Red".to_string() };
let mut jwt_string = jwt.generate_from_payload(&payload).unwrap().1;
jwt_string.push('1');
let result: Result<super::JWT<MyTestClaym>, super::LsError> = jwt.parse_token(&jwt_string);
let mut is_invalid = false;
match result {
Ok(r) => println!("Ok: {r:?}"),
Err(e) => match e {
super::LsError::InvalidTokenError { message: mes } => {
println!("Error message: {:?}", &mes);
is_invalid = true;
}
_ => println!("Other kind of error: {e:?}"),
},
};
assert!(is_invalid)
}
#[test]
fn should_fail_parsing_expired_token() {
let jwt = new();
let token = super::JWT {
payload: MyTestClaym { id: Local::now().timestamp(), name: "Red".to_string() },
sub: "".to_string(),
exp: Local::now().timestamp() - 10,
iat: Local::now().timestamp() - 100,
};
let jwt_string = jwt.generate_from_token(&token).unwrap();
let result: Result<MyTestClaym, super::LsError> = jwt.parse_payload(&jwt_string);
let mut is_expired = false;
match result {
Ok(r) => println!("Ok: {r:?}"),
Err(e) => match e {
super::LsError::ExpiredTokenError { message: mes } => {
println!("Expired: {:?}", &mes);
is_expired = true;
}
_ => println!("Other kind of error: {e:?}"),
},
};
assert!(is_expired)
}
#[test]
fn should_not_build_if_secret_key_empty() {
assert!(
super::LsJwtService::new(&JwtConfig {
secret: "".into(),
signature_algorithm: jsonwebtoken::Algorithm::HS512,
token_validity_minutes: 60,
})
.is_err()
);
}
#[test]
fn should_reject_non_hmac_algorithm() {
for alg in [
jsonwebtoken::Algorithm::RS256,
jsonwebtoken::Algorithm::RS384,
jsonwebtoken::Algorithm::RS512,
jsonwebtoken::Algorithm::ES256,
jsonwebtoken::Algorithm::ES384,
jsonwebtoken::Algorithm::PS256,
jsonwebtoken::Algorithm::EdDSA,
] {
let result = super::LsJwtService::new(&JwtConfig {
secret: "mySecret".into(),
signature_algorithm: alg,
token_validity_minutes: 60,
});
match result {
Err(super::LsError::ConfigurationError { .. }) => {}
Err(other) => panic!("expected ConfigurationError for {alg:?}, got {other:?}"),
Ok(_) => panic!("expected ConfigurationError for {alg:?}, got Ok"),
}
}
}
fn new() -> super::LsJwtService {
super::LsJwtService::new(&JwtConfig {
secret: "mySecret".into(),
signature_algorithm: jsonwebtoken::Algorithm::HS512,
token_validity_minutes: 60,
})
.unwrap()
}
#[derive(Debug, Serialize, Deserialize)]
struct MyTestClaym {
id: i64,
name: String,
}
}