From 844c89a365c53a60c45fe4efbf7ded5ef8b08f19 Mon Sep 17 00:00:00 2001 From: N08I40K Date: Sat, 22 Mar 2025 23:14:14 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=D1=8B=20JWT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Имплементация PartialEq для utils::jwt::VerifyError Замена устаревшего changeset_options на diesel Удалена проверка на ошибку создания токена, так как вероятность её появления близка к нулю --- src/database/models.rs | 3 +- src/routes/auth/sign_in.rs | 5 +- src/utility/jwt.rs | 106 +++++++++++++++++++++++++++++-------- 3 files changed, 88 insertions(+), 26 deletions(-) diff --git a/src/database/models.rs b/src/database/models.rs index 76aec48..ff1bd02 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -1,5 +1,4 @@ use diesel::prelude::*; -use diesel::{AsExpression, FromSqlRow}; use serde::Serialize; #[derive(diesel_derive_enum::DbEnum, Serialize, Debug, Clone, Copy, PartialEq)] @@ -14,7 +13,7 @@ pub enum UserRole { #[derive(Identifiable, AsChangeset, Queryable, Selectable, Serialize)] #[diesel(table_name = crate::database::schema::users)] -#[changeset_options(treat_none_as_null = "true")] +#[diesel(treat_none_as_null = true)] pub struct User { pub id: String, pub username: String, diff --git a/src/routes/auth/sign_in.rs b/src/routes/auth/sign_in.rs index d717d77..430abf7 100644 --- a/src/routes/auth/sign_in.rs +++ b/src/routes/auth/sign_in.rs @@ -16,12 +16,11 @@ pub async fn sign_in(data: Json, app_state: web::Data) -> J let mut lock = app_state.connection(); let conn = lock.deref_mut(); - user.access_token = - utility::jwt::encode(&user.id).expect("Failed to generate jet token"); + user.access_token = utility::jwt::encode(&user.id); user.save_changes::(conn) .expect("Failed to update user"); - + SignInResult::ok(&user) } Ok(false) | Err(_) => SignInResult::err(IncorrectCredentials), diff --git a/src/utility/jwt.rs b/src/utility/jwt.rs index f606379..459db00 100644 --- a/src/utility/jwt.rs +++ b/src/utility/jwt.rs @@ -1,9 +1,13 @@ -use chrono::{DateTime, Duration, Utc}; +use chrono::DateTime; +use chrono::Duration; +use chrono::TimeZone; +use chrono::Utc; use hmac::{Hmac, Mac}; use jwt::{SignWithKey, Token, VerifyWithKey}; use sha2::Sha256; use std::collections::BTreeMap; use std::env; +use std::mem::discriminant; use std::sync::LazyLock; static JWT_SECRET: LazyLock> = LazyLock::new(|| { @@ -16,9 +20,14 @@ static JWT_SECRET: LazyLock> = LazyLock::new(|| { pub enum VerifyError { JwtError(jwt::Error), InvalidSignature, - NoExpirationTag, + InvalidToken, Expired, - NoId, +} + +impl PartialEq for VerifyError { + fn eq(&self, other: &Self) -> bool { + discriminant(self) == discriminant(other) + } } pub fn verify_and_decode(token: &String) -> Result { @@ -27,31 +36,29 @@ pub fn verify_and_decode(token: &String) -> Result { let result: Result, jwt::Error> = token.verify_with_key(jwt); match result { - Ok(claims) => match claims.get("exp") { - None => Err(VerifyError::NoExpirationTag), - Some(exp) => { - let exp_date = DateTime::from_timestamp(exp.parse::().unwrap(), 0) - .expect("Failed to parse expiration time"); + Ok(claims) => { + let exp = claims.get("exp").unwrap(); + let exp_date = DateTime::from_timestamp(exp.parse::().unwrap(), 0) + .expect("Failed to parse expiration time"); - if Utc::now() > exp_date { - return Err(VerifyError::Expired); - } - - match claims.get("id").cloned() { - None => Err(VerifyError::NoId), - Some(id) => Ok(id), - } + if Utc::now() > exp_date { + return Err(VerifyError::Expired); } - }, + + Ok(claims.get("id").cloned().unwrap()) + } Err(err) => Err(match err { jwt::Error::InvalidSignature => VerifyError::InvalidSignature, + jwt::Error::Format | jwt::Error::Base64(_) | jwt::Error::NoClaimsComponent => { + VerifyError::InvalidToken + } _ => VerifyError::JwtError(err), }), } } -pub fn encode(id: &String) -> Result { +pub fn encode(id: &String) -> String { let header = jwt::Header { type_: Some(jwt::header::HeaderType::JsonWebToken), ..Default::default() @@ -69,8 +76,65 @@ pub fn encode(id: &String) -> Result { claims.insert("iat", iat_str.as_str()); claims.insert("exp", exp_str.as_str()); - match Token::new(header, claims).sign_with_key(&*JWT_SECRET) { - Ok(token) => Ok(token.as_str().to_string()), - Err(err) => Err(err), + Token::new(header, claims) + .sign_with_key(&*JWT_SECRET) + .unwrap() + .as_str() + .to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + use dotenvy::dotenv; + + #[test] + fn test_encode() { + dotenv().unwrap(); + + assert_eq!(encode(&"test".to_string()).is_empty(), false); + } + + #[test] + fn test_decode_invalid_token() { + dotenv().unwrap(); + + let token = "".to_string(); + let result = verify_and_decode(&token); + + assert!(result.is_err()); + assert_eq!(result.err().unwrap(), VerifyError::InvalidToken); + } + + #[test] + fn test_decode_invalid_signature() { + dotenv().unwrap(); + + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxODY4ODEyOTI4IiwiaWF0IjoiMTc0MjY2ODkyOCIsImlkIjoiNjdkY2M5YTk1MDdiMDAwMDc3Mjc0NGEyIn0.DQYFYF-3DoJgCLOVdAWa47nUaCJAh16DXj-ChNSSmWz".to_string(); + let result = verify_and_decode(&token); + + assert!(result.is_err()); + assert_eq!(result.err().unwrap(), VerifyError::InvalidToken); + } + + #[test] + fn test_decode_expired() { + dotenv().unwrap(); + + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxNjE2NTI2Mzc2IiwiaWF0IjoiMTQ5MDM4MjM3NiIsImlkIjoiNjdkY2M5YTk1MDdiMDAwMDc3Mjc0NGEyIn0.Qc2LbMJTvl2hWzDM2XyQv4m9lIqR84COAESQAieUxz8".to_string(); + let result = verify_and_decode(&token); + + assert!(result.is_err()); + assert_eq!(result.err().unwrap(), VerifyError::Expired); + } + + #[test] + fn test_decode_ok() { + dotenv().unwrap(); + + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxODY4ODEyOTI4IiwiaWF0IjoiMTc0MjY2ODkyOCIsImlkIjoiNjdkY2M5YTk1MDdiMDAwMDc3Mjc0NGEyIn0.DQYFYF-3DoJgCLOVdAWa47nUaCJAh16DXj-ChNSSmWw".to_string(); + let result = verify_and_decode(&token); + + assert!(result.is_ok()); } }