From 5396301913081173329ce44259abfde04ab9c0dc Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 22 May 2023 16:20:17 +0100 Subject: [PATCH] refactor: [#157] extract authentication service Decoupling services from actix-web framework. --- src/app.rs | 31 +++-- src/auth.rs | 58 ++------ src/common.rs | 16 ++- src/databases/database.rs | 2 +- src/databases/mysql.rs | 2 +- src/databases/sqlite.rs | 2 +- src/routes/user.rs | 140 +++---------------- src/services/authentication.rs | 242 +++++++++++++++++++++++++++++++++ src/services/mod.rs | 1 + 9 files changed, 306 insertions(+), 188 deletions(-) create mode 100644 src/services/authentication.rs diff --git a/src/app.rs b/src/app.rs index 2a7d3d6b..3aa8e29e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,12 +6,13 @@ use actix_web::dev::Server; use actix_web::{middleware, web, App, HttpServer}; use log::info; -use crate::auth::AuthorizationService; +use crate::auth::Authentication; use crate::bootstrap::logging; use crate::cache::image::manager::ImageCacheService; use crate::common::AppData; use crate::config::Configuration; use crate::databases::database; +use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebToken, Service}; use crate::services::category::{self, DbCategoryRepository}; use crate::services::torrent::{ DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator, @@ -50,15 +51,13 @@ pub async fn run(configuration: Configuration) -> Running { // Build app dependencies let database = Arc::new(database::connect(&database_connect_url).await.expect("Database error.")); - let auth = Arc::new(AuthorizationService::new(configuration.clone(), database.clone())); - let tracker_service = Arc::new(tracker::service::Service::new(configuration.clone(), database.clone()).await); - let tracker_statistics_importer = - Arc::new(StatisticsImporter::new(configuration.clone(), tracker_service.clone(), database.clone()).await); - let mailer_service = Arc::new(mailer::Service::new(configuration.clone()).await); - let image_cache_service: Arc = Arc::new(ImageCacheService::new(configuration.clone()).await); + let json_web_token = Arc::new(JsonWebToken::new(configuration.clone())); + let auth = Arc::new(Authentication::new(json_web_token.clone())); + // Repositories let category_repository = Arc::new(DbCategoryRepository::new(database.clone())); let user_repository = Arc::new(DbUserRepository::new(database.clone())); + let user_authentication_repository = Arc::new(DbUserAuthenticationRepository::new(database.clone())); let user_profile_repository = Arc::new(DbUserProfileRepository::new(database.clone())); let torrent_repository = Arc::new(DbTorrentRepository::new(database.clone())); let torrent_info_repository = Arc::new(DbTorrentInfoRepository::new(database.clone())); @@ -66,7 +65,13 @@ pub async fn run(configuration: Configuration) -> Running { let torrent_announce_url_repository = Arc::new(DbTorrentAnnounceUrlRepository::new(database.clone())); let torrent_listing_generator = Arc::new(DbTorrentListingGenerator::new(database.clone())); let banned_user_list = Arc::new(DbBannedUserList::new(database.clone())); + // Services + let tracker_service = Arc::new(tracker::service::Service::new(configuration.clone(), database.clone()).await); + let tracker_statistics_importer = + Arc::new(StatisticsImporter::new(configuration.clone(), tracker_service.clone(), database.clone()).await); + let mailer_service = Arc::new(mailer::Service::new(configuration.clone()).await); + let image_cache_service: Arc = Arc::new(ImageCacheService::new(configuration.clone()).await); let category_service = Arc::new(category::Service::new(category_repository.clone(), user_repository.clone())); let proxy_service = Arc::new(proxy::Service::new(image_cache_service.clone(), user_repository.clone())); let settings_service = Arc::new(settings::Service::new(configuration.clone(), user_repository.clone())); @@ -93,20 +98,29 @@ pub async fn run(configuration: Configuration) -> Running { user_profile_repository.clone(), banned_user_list.clone(), )); + let authentication_service = Arc::new(Service::new( + configuration.clone(), + json_web_token.clone(), + user_repository.clone(), + user_profile_repository.clone(), + user_authentication_repository.clone(), + )); // Build app container let app_data = Arc::new(AppData::new( configuration.clone(), database.clone(), + json_web_token.clone(), auth.clone(), + authentication_service, tracker_service.clone(), tracker_statistics_importer.clone(), mailer_service, image_cache_service, - // Repositories category_repository, user_repository, + user_authentication_repository, user_profile_repository, torrent_repository, torrent_info_repository, @@ -114,7 +128,6 @@ pub async fn run(configuration: Configuration) -> Running { torrent_announce_url_repository, torrent_listing_generator, banned_user_list, - // Services category_service, proxy_service, settings_service, diff --git a/src/auth.rs b/src/auth.rs index 609496d4..722cf2f1 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,36 +1,24 @@ use std::sync::Arc; use actix_web::HttpRequest; -use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; -use crate::config::Configuration; -use crate::databases::database::Database; use crate::errors::ServiceError; use crate::models::user::{UserClaims, UserCompact, UserId}; -use crate::utils::clock; +use crate::services::authentication::JsonWebToken; -pub struct AuthorizationService { - cfg: Arc, - database: Arc>, +pub struct Authentication { + json_web_token: Arc, } -impl AuthorizationService { - pub fn new(cfg: Arc, database: Arc>) -> AuthorizationService { - AuthorizationService { cfg, database } +impl Authentication { + #[must_use] + pub fn new(json_web_token: Arc) -> Self { + Self { json_web_token } } /// Create Json Web Token pub async fn sign_jwt(&self, user: UserCompact) -> String { - let settings = self.cfg.settings.read().await; - - // create JWT that expires in two weeks - let key = settings.auth.secret_key.as_bytes(); - // TODO: create config option for setting the token validity in seconds - let exp_date = clock::now() + 1_209_600; // two weeks from now - - let claims = UserClaims { user, exp: exp_date }; - - encode(&Header::default(), &claims, &EncodingKey::from_secret(key)).expect("argument `Header` should match `EncodingKey`") + self.json_web_token.sign(user).await } /// Verify Json Web Token @@ -39,21 +27,7 @@ impl AuthorizationService { /// /// This function will return an error if the JWT is not good or expired. pub async fn verify_jwt(&self, token: &str) -> Result { - let settings = self.cfg.settings.read().await; - - match decode::( - token, - &DecodingKey::from_secret(settings.auth.secret_key.as_bytes()), - &Validation::new(Algorithm::HS256), - ) { - Ok(token_data) => { - if token_data.claims.exp < clock::now() { - return Err(ServiceError::TokenExpired); - } - Ok(token_data.claims) - } - Err(_) => Err(ServiceError::TokenInvalid), - } + self.json_web_token.verify(token).await } /// Get Claims from Request @@ -81,20 +55,6 @@ impl AuthorizationService { } } - /// Get User (in compact form) from Request - /// - /// # Errors - /// - /// This function will return an `ServiceError::UserNotFound` if unable to get user from database. - pub async fn get_user_compact_from_request(&self, req: &HttpRequest) -> Result { - let claims = self.get_claims_from_request(req).await?; - - self.database - .get_user_compact_from_id(claims.user.user_id) - .await - .map_err(|_| ServiceError::UserNotFound) - } - /// Get User id from Request /// /// # Errors diff --git a/src/common.rs b/src/common.rs index 5db886f3..4faa4cae 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,9 +1,10 @@ use std::sync::Arc; -use crate::auth::AuthorizationService; +use crate::auth::Authentication; use crate::cache::image::manager::ImageCacheService; use crate::config::Configuration; use crate::databases::database::Database; +use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebToken, Service}; use crate::services::category::{self, DbCategoryRepository}; use crate::services::torrent::{ DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator, @@ -20,7 +21,9 @@ pub type WebAppData = actix_web::web::Data>; pub struct AppData { pub cfg: Arc, pub database: Arc>, - pub auth: Arc, + pub json_web_token: Arc, + pub auth: Arc, + pub authentication_service: Arc, pub tracker_service: Arc, pub tracker_statistics_importer: Arc, pub mailer: Arc, @@ -28,6 +31,7 @@ pub struct AppData { // Repositories pub category_repository: Arc, pub user_repository: Arc, + pub user_authentication_repository: Arc, pub user_profile_repository: Arc, pub torrent_repository: Arc, pub torrent_info_repository: Arc, @@ -49,7 +53,9 @@ impl AppData { pub fn new( cfg: Arc, database: Arc>, - auth: Arc, + json_web_token: Arc, + auth: Arc, + authentication_service: Arc, tracker_service: Arc, tracker_statistics_importer: Arc, mailer: Arc, @@ -57,6 +63,7 @@ impl AppData { // Repositories category_repository: Arc, user_repository: Arc, + user_authentication_repository: Arc, user_profile_repository: Arc, torrent_repository: Arc, torrent_info_repository: Arc, @@ -75,7 +82,9 @@ impl AppData { AppData { cfg, database, + json_web_token, auth, + authentication_service, tracker_service, tracker_statistics_importer, mailer, @@ -83,6 +92,7 @@ impl AppData { // Repositories category_repository, user_repository, + user_authentication_repository, user_profile_repository, torrent_repository, torrent_info_repository, diff --git a/src/databases/database.rs b/src/databases/database.rs index 303e1061..8ac719c6 100644 --- a/src/databases/database.rs +++ b/src/databases/database.rs @@ -107,7 +107,7 @@ pub trait Database: Sync + Send { async fn get_user_from_id(&self, user_id: i64) -> Result; /// Get `UserAuthentication` from `user_id`. - async fn get_user_authentication_from_id(&self, user_id: i64) -> Result; + async fn get_user_authentication_from_id(&self, user_id: UserId) -> Result; /// Get `UserProfile` from `username`. async fn get_user_profile_from_username(&self, username: &str) -> Result; diff --git a/src/databases/mysql.rs b/src/databases/mysql.rs index 74557175..5e3206db 100644 --- a/src/databases/mysql.rs +++ b/src/databases/mysql.rs @@ -107,7 +107,7 @@ impl Database for Mysql { .map_err(|_| database::Error::UserNotFound) } - async fn get_user_authentication_from_id(&self, user_id: i64) -> Result { + async fn get_user_authentication_from_id(&self, user_id: UserId) -> Result { query_as::<_, UserAuthentication>("SELECT * FROM torrust_user_authentication WHERE user_id = ?") .bind(user_id) .fetch_one(&self.pool) diff --git a/src/databases/sqlite.rs b/src/databases/sqlite.rs index 0bc6ddd1..31bec6a2 100644 --- a/src/databases/sqlite.rs +++ b/src/databases/sqlite.rs @@ -108,7 +108,7 @@ impl Database for Sqlite { .map_err(|_| database::Error::UserNotFound) } - async fn get_user_authentication_from_id(&self, user_id: i64) -> Result { + async fn get_user_authentication_from_id(&self, user_id: UserId) -> Result { query_as::<_, UserAuthentication>("SELECT * FROM torrust_user_authentication WHERE user_id = ?") .bind(user_id) .fetch_one(&self.pool) diff --git a/src/routes/user.rs b/src/routes/user.rs index 5c5973da..5912334a 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -1,14 +1,10 @@ use actix_web::{web, HttpRequest, HttpResponse, Responder}; -use argon2::{Argon2, PasswordHash, PasswordVerifier}; -use pbkdf2::Pbkdf2; use serde::{Deserialize, Serialize}; use crate::common::WebAppData; use crate::errors::{ServiceError, ServiceResult}; use crate::models::response::{OkResponse, TokenResponse}; -use crate::models::user::UserAuthentication; use crate::routes::API_VERSION; -use crate::utils::clock; pub fn init(cfg: &mut web::ServiceConfig) { cfg.service( @@ -21,9 +17,9 @@ pub fn init(cfg: &mut web::ServiceConfig) { // The wep app can user this endpoint to verify the email and render the page accordingly. .service(web::resource("/email/verify/{token}").route(web::get().to(email_verification_handler))) // Authentication - .service(web::resource("/login").route(web::post().to(login))) - .service(web::resource("/token/verify").route(web::post().to(verify_token))) - .service(web::resource("/token/renew").route(web::post().to(renew_token))) + .service(web::resource("/login").route(web::post().to(login_handler))) + .service(web::resource("/token/verify").route(web::post().to(verify_token_handler))) + .service(web::resource("/token/renew").route(web::post().to(renew_token_handler))) // User Access Ban // code-review: should not this be a POST method? We add the user to the blacklist. We do not delete the user. .service(web::resource("/ban/{user}").route(web::delete().to(ban_handler))), @@ -76,42 +72,12 @@ pub async fn registration_handler( /// /// # Errors /// -/// This function will return a `ServiceError::WrongPasswordOrUsername` if unable to get user profile. -/// This function will return a `ServiceError::InternalServerError` if unable to get user authentication data from the user id. -/// This function will return an error if unable to verify the password. -/// This function will return a `ServiceError::EmailNotVerified` if the email should be, but is not verified. -/// This function will return an error if unable to get the user data from the database. -pub async fn login(payload: web::Json, app_data: WebAppData) -> ServiceResult { - // get the user profile from database - let user_profile = app_data - .database - .get_user_profile_from_username(&payload.login) - .await - .map_err(|_| ServiceError::WrongPasswordOrUsername)?; - - // should not be able to fail if user_profile succeeded - let user_authentication = app_data - .database - .get_user_authentication_from_id(user_profile.user_id) - .await - .map_err(|_| ServiceError::InternalServerError)?; - - verify_password(payload.password.as_bytes(), &user_authentication)?; - - let settings = app_data.cfg.settings.read().await; - - // fail login if email verification is required and this email is not verified - if settings.mail.email_verification_enabled && !user_profile.email_verified { - return Err(ServiceError::EmailNotVerified); - } - - // drop read lock on settings - drop(settings); - - let user_compact = app_data.database.get_user_compact_from_id(user_profile.user_id).await?; - - // sign jwt with compact user details as payload - let token = app_data.auth.sign_jwt(user_compact.clone()).await; +/// This function will return an error if the user could not be logged in. +pub async fn login_handler(payload: web::Json, app_data: WebAppData) -> ServiceResult { + let (token, user_compact) = app_data + .authentication_service + .login(&payload.login, &payload.password) + .await?; Ok(HttpResponse::Ok().json(OkResponse { data: TokenResponse { @@ -122,43 +88,14 @@ pub async fn login(payload: web::Json, app_data: WebAppData) -> ServiceRe })) } -/// Verify if the user supplied and the database supplied passwords match -/// -/// # Errors -/// -/// This function will return an error if unable to parse password hash from the stored user authentication value. -/// This function will return a `ServiceError::WrongPasswordOrUsername` if unable to match the password with either `argon2id` or `pbkdf2-sha256`. -pub fn verify_password(password: &[u8], user_authentication: &UserAuthentication) -> Result<(), ServiceError> { - // wrap string of the hashed password into a PasswordHash struct for verification - let parsed_hash = PasswordHash::new(&user_authentication.password_hash)?; - - match parsed_hash.algorithm.as_str() { - "argon2id" => { - if Argon2::default().verify_password(password, &parsed_hash).is_err() { - return Err(ServiceError::WrongPasswordOrUsername); - } - - Ok(()) - } - "pbkdf2-sha256" => { - if Pbkdf2.verify_password(password, &parsed_hash).is_err() { - return Err(ServiceError::WrongPasswordOrUsername); - } - - Ok(()) - } - _ => Err(ServiceError::WrongPasswordOrUsername), - } -} - /// Verify a supplied JWT. /// /// # Errors /// /// This function will return an error if unable to verify the supplied payload as a valid jwt. -pub async fn verify_token(payload: web::Json, app_data: WebAppData) -> ServiceResult { - // verify if token is valid - let _claims = app_data.auth.verify_jwt(&payload.token).await?; +pub async fn verify_token_handler(payload: web::Json, app_data: WebAppData) -> ServiceResult { + // Verify if JWT is valid + let _claims = app_data.json_web_token.verify(&payload.token).await?; Ok(HttpResponse::Ok().json(OkResponse { data: "Token is valid.".to_string(), @@ -169,21 +106,10 @@ pub async fn verify_token(payload: web::Json, app_data: WebAppData) -> Se /// /// # Errors /// -/// This function will return an error if unable to verify the supplied payload as a valid jwt. -/// This function will return an error if unable to get user data from the database. -pub async fn renew_token(payload: web::Json, app_data: WebAppData) -> ServiceResult { - const ONE_WEEK_IN_SECONDS: u64 = 604_800; - - // verify if token is valid - let claims = app_data.auth.verify_jwt(&payload.token).await?; - - let user_compact = app_data.database.get_user_compact_from_id(claims.user.user_id).await?; - - // renew token if it is valid for less than one week - let token = match claims.exp - clock::now() { - x if x < ONE_WEEK_IN_SECONDS => app_data.auth.sign_jwt(user_compact.clone()).await, - _ => payload.token.clone(), - }; +/// This function will return an error if unable to verify the supplied +/// payload as a valid JWT. +pub async fn renew_token_handler(payload: web::Json, app_data: WebAppData) -> ServiceResult { + let (token, user_compact) = app_data.authentication_service.renew_token(&payload.token).await?; Ok(HttpResponse::Ok().json(OkResponse { data: TokenResponse { @@ -224,37 +150,3 @@ pub async fn ban_handler(req: HttpRequest, app_data: WebAppData) -> ServiceResul data: format!("Banned user: {to_be_banned_username}"), })) } - -#[cfg(test)] -mod tests { - use super::verify_password; - use crate::models::user::UserAuthentication; - - #[test] - fn password_hashed_with_pbkdf2_sha256_should_be_verified() { - let password = "12345678".as_bytes(); - let password_hash = - "$pbkdf2-sha256$i=10000,l=32$pZIh8nilm+cg6fk5Ubf2zQ$AngLuZ+sGUragqm4bIae/W+ior0TWxYFFaTx8CulqtY".to_string(); - let user_authentication = UserAuthentication { - user_id: 1i64, - password_hash, - }; - - assert!(verify_password(password, &user_authentication).is_ok()); - assert!(verify_password("incorrect password".as_bytes(), &user_authentication).is_err()); - } - - #[test] - fn password_hashed_with_argon2_should_be_verified() { - let password = "87654321".as_bytes(); - let password_hash = - "$argon2id$v=19$m=4096,t=3,p=1$ycK5lJ4xmFBnaJ51M1j1eA$kU3UlNiSc3JDbl48TCj7JBDKmrT92DOUAgo4Yq0+nMw".to_string(); - let user_authentication = UserAuthentication { - user_id: 1i64, - password_hash, - }; - - assert!(verify_password(password, &user_authentication).is_ok()); - assert!(verify_password("incorrect password".as_bytes(), &user_authentication).is_err()); - } -} diff --git a/src/services/authentication.rs b/src/services/authentication.rs new file mode 100644 index 00000000..5f792c87 --- /dev/null +++ b/src/services/authentication.rs @@ -0,0 +1,242 @@ +use std::sync::Arc; + +use argon2::{Argon2, PasswordHash, PasswordVerifier}; +use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; +use pbkdf2::Pbkdf2; + +use super::user::{DbUserProfileRepository, DbUserRepository}; +use crate::config::Configuration; +use crate::databases::database::{Database, Error}; +use crate::errors::ServiceError; +use crate::models::user::{UserAuthentication, UserClaims, UserCompact, UserId}; +use crate::utils::clock; + +pub struct Service { + configuration: Arc, + json_web_token: Arc, + user_repository: Arc, + user_profile_repository: Arc, + user_authentication_repository: Arc, +} + +impl Service { + pub fn new( + configuration: Arc, + json_web_token: Arc, + user_repository: Arc, + user_profile_repository: Arc, + user_authentication_repository: Arc, + ) -> Self { + Self { + configuration, + json_web_token, + user_repository, + user_profile_repository, + user_authentication_repository, + } + } + + /// Authenticate user with username and password. + /// It returns a JWT token and a compact user profile. + /// + /// # Errors + /// + /// It returns: + /// + /// * A `ServiceError::WrongPasswordOrUsername` if unable to get user profile. + /// * A `ServiceError::InternalServerError` if unable to get user authentication data from the user id. + /// * A `ServiceError::EmailNotVerified` if the email should be, but is not verified. + /// * An error if unable to verify the password. + /// * An error if unable to get the user data from the database. + pub async fn login(&self, username: &str, password: &str) -> Result<(String, UserCompact), ServiceError> { + // Get the user profile from database + let user_profile = self + .user_profile_repository + .get_user_profile_from_username(username) + .await + .map_err(|_| ServiceError::WrongPasswordOrUsername)?; + + // Should not be able to fail if user_profile succeeded + let user_authentication = self + .user_authentication_repository + .get_user_authentication_from_id(&user_profile.user_id) + .await + .map_err(|_| ServiceError::InternalServerError)?; + + verify_password(password.as_bytes(), &user_authentication)?; + + let settings = self.configuration.settings.read().await; + + // Fail login if email verification is required and this email is not verified + if settings.mail.email_verification_enabled && !user_profile.email_verified { + return Err(ServiceError::EmailNotVerified); + } + + // Drop read lock on settings + drop(settings); + + let user_compact = self.user_repository.get_compact(&user_profile.user_id).await?; + + // Sign JWT with compact user details as payload + let token = self.json_web_token.sign(user_compact.clone()).await; + + Ok((token, user_compact)) + } + + /// Renew a supplied JWT. + /// + /// # Errors + /// + /// This function will return an error if: + /// + /// * Unable to verify the supplied payload as a valid jwt. + /// * Unable to get user data from the database. + pub async fn renew_token(&self, token: &str) -> Result<(String, UserCompact), ServiceError> { + const ONE_WEEK_IN_SECONDS: u64 = 604_800; + + // Verify if token is valid + let claims = self.json_web_token.verify(token).await?; + + let user_compact = self.user_repository.get_compact(&claims.user.user_id).await?; + + // Renew token if it is valid for less than one week + let token = match claims.exp - clock::now() { + x if x < ONE_WEEK_IN_SECONDS => self.json_web_token.sign(user_compact.clone()).await, + _ => token.clone().to_owned(), + }; + + Ok((token, user_compact)) + } +} + +pub struct JsonWebToken { + cfg: Arc, +} + +impl JsonWebToken { + pub fn new(cfg: Arc) -> Self { + Self { cfg } + } + + /// Create Json Web Token. + pub async fn sign(&self, user: UserCompact) -> String { + let settings = self.cfg.settings.read().await; + + // Create JWT that expires in two weeks + let key = settings.auth.secret_key.as_bytes(); + + // todo: create config option for setting the token validity in seconds. + let exp_date = clock::now() + 1_209_600; // two weeks from now + + let claims = UserClaims { user, exp: exp_date }; + + encode(&Header::default(), &claims, &EncodingKey::from_secret(key)).expect("argument `Header` should match `EncodingKey`") + } + + /// Verify Json Web Token. + /// + /// # Errors + /// + /// This function will return an error if the JWT is not good or expired. + pub async fn verify(&self, token: &str) -> Result { + let settings = self.cfg.settings.read().await; + + match decode::( + token, + &DecodingKey::from_secret(settings.auth.secret_key.as_bytes()), + &Validation::new(Algorithm::HS256), + ) { + Ok(token_data) => { + if token_data.claims.exp < clock::now() { + return Err(ServiceError::TokenExpired); + } + Ok(token_data.claims) + } + Err(_) => Err(ServiceError::TokenInvalid), + } + } +} + +pub struct DbUserAuthenticationRepository { + database: Arc>, +} + +impl DbUserAuthenticationRepository { + #[must_use] + pub fn new(database: Arc>) -> Self { + Self { database } + } + + /// Get user authentication data from user id. + /// + /// # Errors + /// + /// This function will return an error if unable to get the user + /// authentication data from the database. + pub async fn get_user_authentication_from_id(&self, user_id: &UserId) -> Result { + self.database.get_user_authentication_from_id(*user_id).await + } +} + +/// Verify if the user supplied and the database supplied passwords match +/// +/// # Errors +/// +/// This function will return an error if unable to parse password hash from the stored user authentication value. +/// This function will return a `ServiceError::WrongPasswordOrUsername` if unable to match the password with either `argon2id` or `pbkdf2-sha256`. +fn verify_password(password: &[u8], user_authentication: &UserAuthentication) -> Result<(), ServiceError> { + // wrap string of the hashed password into a PasswordHash struct for verification + let parsed_hash = PasswordHash::new(&user_authentication.password_hash)?; + + match parsed_hash.algorithm.as_str() { + "argon2id" => { + if Argon2::default().verify_password(password, &parsed_hash).is_err() { + return Err(ServiceError::WrongPasswordOrUsername); + } + + Ok(()) + } + "pbkdf2-sha256" => { + if Pbkdf2.verify_password(password, &parsed_hash).is_err() { + return Err(ServiceError::WrongPasswordOrUsername); + } + + Ok(()) + } + _ => Err(ServiceError::WrongPasswordOrUsername), + } +} + +#[cfg(test)] +mod tests { + use super::verify_password; + use crate::models::user::UserAuthentication; + + #[test] + fn password_hashed_with_pbkdf2_sha256_should_be_verified() { + let password = "12345678".as_bytes(); + let password_hash = + "$pbkdf2-sha256$i=10000,l=32$pZIh8nilm+cg6fk5Ubf2zQ$AngLuZ+sGUragqm4bIae/W+ior0TWxYFFaTx8CulqtY".to_string(); + let user_authentication = UserAuthentication { + user_id: 1i64, + password_hash, + }; + + assert!(verify_password(password, &user_authentication).is_ok()); + assert!(verify_password("incorrect password".as_bytes(), &user_authentication).is_err()); + } + + #[test] + fn password_hashed_with_argon2_should_be_verified() { + let password = "87654321".as_bytes(); + let password_hash = + "$argon2id$v=19$m=4096,t=3,p=1$ycK5lJ4xmFBnaJ51M1j1eA$kU3UlNiSc3JDbl48TCj7JBDKmrT92DOUAgo4Yq0+nMw".to_string(); + let user_authentication = UserAuthentication { + user_id: 1i64, + password_hash, + }; + + assert!(verify_password(password, &user_authentication).is_ok()); + assert!(verify_password("incorrect password".as_bytes(), &user_authentication).is_err()); + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs index 306931e0..e298313e 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,4 +1,5 @@ pub mod about; +pub mod authentication; pub mod category; pub mod proxy; pub mod settings;