mirror of
https://github.com/n08i40k/schedule-parser-rusted.git
synced 2025-12-06 17:57:47 +03:00
172 lines
4.7 KiB
Rust
172 lines
4.7 KiB
Rust
use chrono::Duration;
|
|
use chrono::Utc;
|
|
use jsonwebtoken::errors::ErrorKind;
|
|
use jsonwebtoken::{decode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_with::serde_as;
|
|
use serde_with::DisplayFromStr;
|
|
use std::env;
|
|
use std::mem::discriminant;
|
|
use std::sync::LazyLock;
|
|
use database::entity::UserType;
|
|
|
|
/// Key for token verification.
|
|
static DECODING_KEY: LazyLock<DecodingKey> = LazyLock::new(|| {
|
|
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
|
|
|
DecodingKey::from_secret(secret.as_bytes())
|
|
});
|
|
|
|
/// Key for creating a signed token.
|
|
static ENCODING_KEY: LazyLock<EncodingKey> = LazyLock::new(|| {
|
|
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
|
|
|
EncodingKey::from_secret(secret.as_bytes())
|
|
});
|
|
|
|
/// Token verification errors.
|
|
#[derive(Debug)]
|
|
pub enum Error {
|
|
/// The token has a different signature.
|
|
InvalidSignature,
|
|
|
|
/// Token reading error.
|
|
InvalidToken,
|
|
|
|
/// Token expired.
|
|
Expired,
|
|
}
|
|
|
|
impl PartialEq for Error {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
discriminant(self) == discriminant(other)
|
|
}
|
|
}
|
|
|
|
|
|
/// The data the token holds.
|
|
#[serde_as]
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct Claims {
|
|
/// User account UUID.
|
|
pub id: String,
|
|
|
|
/// User type.
|
|
pub user_type: Option<UserType>,
|
|
|
|
/// Token creation date.
|
|
#[serde_as(as = "DisplayFromStr")]
|
|
pub iat: u64,
|
|
|
|
/// Token expiry date.
|
|
#[serde_as(as = "DisplayFromStr")]
|
|
pub exp: u64,
|
|
}
|
|
|
|
/// Token signing algorithm.
|
|
pub(crate) const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256;
|
|
|
|
/// Checking the token and extracting the UUID of the user account from it.
|
|
pub fn verify_and_decode(token: &str) -> Result<Claims, Error> {
|
|
let mut validation = Validation::new(DEFAULT_ALGORITHM);
|
|
|
|
validation.required_spec_claims.remove("exp");
|
|
validation.validate_exp = false;
|
|
|
|
let result = decode::<Claims>(token, &DECODING_KEY, &validation);
|
|
|
|
match result {
|
|
Ok(token_data) => {
|
|
if token_data.claims.exp < Utc::now().timestamp().unsigned_abs() {
|
|
Err(Error::Expired)
|
|
} else {
|
|
Ok(token_data.claims)
|
|
}
|
|
}
|
|
Err(err) => Err(match err.into_kind() {
|
|
ErrorKind::InvalidSignature => Error::InvalidSignature,
|
|
ErrorKind::ExpiredSignature => Error::Expired,
|
|
_ => Error::InvalidToken,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Creating a user token.
|
|
pub fn encode(user_type: UserType, id: &str) -> String {
|
|
let header = Header {
|
|
typ: Some(String::from("JWT")),
|
|
..Default::default()
|
|
};
|
|
|
|
let iat = Utc::now();
|
|
let exp = iat + Duration::days(365 * 4);
|
|
|
|
let claims = Claims {
|
|
id: id.to_string(),
|
|
user_type: Some(user_type),
|
|
iat: iat.timestamp().unsigned_abs(),
|
|
exp: exp.timestamp().unsigned_abs(),
|
|
};
|
|
|
|
jsonwebtoken::encode(&header, &claims, &ENCODING_KEY).unwrap()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::test_env::tests::test_env;
|
|
|
|
#[test]
|
|
fn test_encode() {
|
|
test_env();
|
|
|
|
assert!(!encode(UserType::Default, "test").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_decode_invalid_token() {
|
|
test_env();
|
|
|
|
let token = "".to_string();
|
|
let result = verify_and_decode(&token);
|
|
|
|
assert!(result.is_err());
|
|
assert_eq!(result.err().unwrap(), Error::InvalidToken);
|
|
}
|
|
|
|
//noinspection SpellCheckingInspection
|
|
#[test]
|
|
fn test_decode_invalid_signature() {
|
|
test_env();
|
|
|
|
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxNjE2NTI2Mzc2IiwiaWF0IjoiMTQ5MDM4MjM3NiIsImlkIjoiNjdkY2M5YTk1MDdiMDAwMDc3Mjc0NGEyIn0.Qc2LbMJTvl2hWzDM2XyQv4m9lIqR84COAESQAieUxz8".to_string();
|
|
let result = verify_and_decode(&token);
|
|
|
|
assert!(result.is_err());
|
|
assert_eq!(result.err().unwrap(), Error::InvalidSignature);
|
|
}
|
|
|
|
//noinspection SpellCheckingInspection
|
|
#[test]
|
|
fn test_decode_expired() {
|
|
test_env();
|
|
|
|
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3ZGNjOWE5NTA3YjAwMDA3NzI3NDRhMiIsImlhdCI6IjAiLCJleHAiOiIwIn0.GBsVYvnZIfHXt00t-qmAdUMyHSyWOBtC0Mrxwg1HQOM".to_string();
|
|
let result = verify_and_decode(&token);
|
|
|
|
assert!(result.is_err());
|
|
assert_eq!(result.err().unwrap(), Error::Expired);
|
|
}
|
|
|
|
//noinspection SpellCheckingInspection
|
|
#[test]
|
|
fn test_decode_ok() {
|
|
test_env();
|
|
|
|
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3ZGNjOWE5NTA3YjAwMDA3NzI3NDRhMiIsImlhdCI6Ijk5OTk5OTk5OTkiLCJleHAiOiI5OTk5OTk5OTk5In0.o1vN-ze5iaJrnlHqe7WARXMBhhzjxTjTKkjlmTGEnOI".to_string();
|
|
let result = verify_and_decode(&token);
|
|
|
|
assert!(result.is_ok());
|
|
}
|
|
}
|