diff --git a/src/core/auth.rs b/src/core/auth.rs new file mode 100644 index 0000000..cd59f75 --- /dev/null +++ b/src/core/auth.rs @@ -0,0 +1,147 @@ +use bson::doc; +use mongodb::{Client, Collection}; + +use crate::{ + core::{dek::Dek, session::Session, user::User}, + errors::{Error, Result}, + models::auth_model::{SessionResponseForSignInOrSignUp, SignInOrSignUpResponse}, + utils::{encryption_utils::Encryption, password_utils::Password}, +}; + +pub struct Auth; + +impl Auth { + pub async fn sign_up( + mongo_client: &Client, + name: &str, + email: &str, + role: &str, + password: &str, + ) -> Result { + println!(">> HANDLER: add_user_handler called"); + + let db = mongo_client.database("test"); + + let collection: Collection = db.collection("users"); + let cursor = collection + .find_one( + Some(doc! { + "email": email + }), + None, + ) + .await + .unwrap(); + + if cursor.is_some() { + return Err(Error::UserAlreadyExists { + message: "User already exists".to_string(), + }); + } + + let dek = Dek::generate(); // create a data encryption key for new user + let user = match User::new(name, email, role, password) + .encrypt_and_add(&mongo_client, &dek) + .await + { + Ok(user) => user, + Err(e) => return Err(e), + }; + + // add the dek to the deks collection + let dek_data = match Dek::new(&user.uid, &user.email, &dek) + .encrypt_and_add(&mongo_client) + .await + { + Ok(dek_data) => dek_data, + Err(e) => return Err(e), + }; + + let session = match Session::new(&user, "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36").encrypt_add(&mongo_client, &dek).await { + Ok(session) => session, + Err(e) => return Err(e), + }; + + Ok(SignInOrSignUpResponse { + message: "Signup successful".to_string(), + uid: user.uid, + name: user.name, + email: user.email, + role: user.role, + created_at: user.created_at, + updated_at: user.updated_at, + email_verified: user.email_verified, + is_active: user.is_active, + session: SessionResponseForSignInOrSignUp { + session_id: Encryption::encrypt_data(&session.session_id, &dek_data.dek), + id_token: session.id_token, + refresh_token: session.refresh_token, + }, + }) + } + + pub async fn sign_in( + mongo_client: &Client, + email: &str, + password: &str, + ) -> Result { + let user = match User::get_from_email(&mongo_client, email).await { + Ok(user) => user, + Err(e) => return Err(e), + }; + + let dek_data = match Dek::get(&mongo_client, &user.uid).await { + Ok(dek_data) => dek_data, + Err(e) => return Err(e), + }; + + // verify the password + if Password::verify_hash(password, &user.password) { + let session = match Session::new(&user, "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36").encrypt_add(&mongo_client, &dek_data.dek).await { + Ok(session) => session, + Err(e) => return Err(e), + }; + // let res = Json(json!({ + // "message": "Signin successful", + // "user": { + // "uid": user.uid, + // "name": user.name, + // "email": user.email, + // "role": user.role, + // "created_at": user.created_at, + // "updated_at": user.updated_at, + // "email_verified": user.email_verified, + // "is_active": user.is_active, + // "session": { + // "session_id": Encryption::encrypt_data(&session.session_id, &dek_data.dek), + // "id_token" : session.id_token, + // "refresh_token" : session.refresh_token, + // }, + // }, + // })); + + let res = SignInOrSignUpResponse { + message: "Signin successful".to_string(), + uid: user.uid, + name: user.name, + email: user.email, + role: user.role, + created_at: user.created_at, + updated_at: user.updated_at, + email_verified: user.email_verified, + is_active: user.is_active, + session: SessionResponseForSignInOrSignUp { + session_id: Encryption::encrypt_data(&session.session_id, &dek_data.dek), + id_token: session.id_token, + refresh_token: session.refresh_token, + }, + }; + + Ok(res) + } else { + Err(Error::UserNotFound { + message: "User not found".to_string(), + }) + } + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs index 2deef07..683eaf3 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,3 +1,4 @@ -pub mod user; +pub mod auth; pub mod dek; -pub mod session; \ No newline at end of file +pub mod session; +pub mod user; diff --git a/src/core/user.rs b/src/core/user.rs index fd3e351..02ec4ec 100644 --- a/src/core/user.rs +++ b/src/core/user.rs @@ -5,7 +5,7 @@ use crate::{ models::{password_model::ForgetPasswordRequest, user_model::UserResponse}, traits::{decryption::Decrypt, encryption::Encrypt}, utils::{ - email_utils::Email, encryption_utils::Encryption, hashing_utils::{salt_and_hash_password, verify_password_hash} + email_utils::Email, encryption_utils::Encryption, password_utils::Password }, }; use bson::{doc, oid::ObjectId, uuid, DateTime}; @@ -48,7 +48,7 @@ impl User { pub async fn encrypt_and_add(&self, mongo_client: &Client, dek: &str) -> Result { let db = mongo_client.database("test"); let mut user = self.clone(); - user.password = salt_and_hash_password(user.password.as_str()); + user.password = Password::salt_and_hash(user.password.as_str()); let collection: Collection = db.collection("users"); match collection.insert_one(user.encrypt(&dek), None).await { Ok(_) => return Ok(user), @@ -61,12 +61,6 @@ impl User { } pub async fn get_from_email(mongo_client: &Client, email: &str) -> Result { - // check if the payload is empty - match email.is_empty() { - true => Err(Error::InvalidPayload { - message: "Invalid payload".to_string(), - }), - false => { let user_collection: Collection = mongo_client.database("test").collection("users"); let dek_data = match Dek::get(&mongo_client, email).await { @@ -98,8 +92,6 @@ impl User { message: "Failed to get User".to_string(), }), } - } - } } pub async fn get_from_uid(mongo_client: &Client, uid: &str) -> Result { @@ -328,13 +320,13 @@ impl User { let decrypted_user = user.decrypt(&dek_data.dek); - if !verify_password_hash(&old_password, &decrypted_user.password) { + if !Password::verify_hash(&old_password, &decrypted_user.password) { return Err(Error::InvalidPassword { message: "New password cannot be the same as the old password".to_string(), }); } // hash and salt the new password - let hashed_and_salted_pass = salt_and_hash_password(&new_password); + let hashed_and_salted_pass = Password::salt_and_hash(&new_password); // encrypt the new password let encrypted_password = Encryption::encrypt_data(&hashed_and_salted_pass, &dek_data.dek); @@ -459,7 +451,7 @@ impl User { }; // hash and salt the new password - let hashed_and_salted_pass = salt_and_hash_password(&new_password); + let hashed_and_salted_pass = Password::salt_and_hash(&new_password); // encrypt the new password let encrypted_password = Encryption::encrypt_data(&hashed_and_salted_pass, &dek_data.dek); diff --git a/src/handlers/auth_handler.rs b/src/handlers/auth_handler.rs index ae1e8a4..4d058d4 100644 --- a/src/handlers/auth_handler.rs +++ b/src/handlers/auth_handler.rs @@ -1,20 +1,44 @@ use axum::{extract::State, Json}; use axum_macros::debug_handler; -use serde_json::Value; use crate::{ - core::session::Session, errors::{Error, Result}, models::{auth_model::{SignInPayload, SignUpPayload}, session_model::{RevokeSessionsPayload, RevokeSessionsResult}}, utils::auth_utils::{sign_in, sign_up}, AppState + core::{auth::Auth, session::Session}, + errors::{Error, Result}, + models::{ + auth_model::{SignInOrSignUpResponse, SignInPayload, SignUpPayload}, + session_model::{RevokeSessionsPayload, RevokeSessionsResult}, + }, + AppState, }; #[debug_handler] pub async fn signup_handler( State(state): State, payload: Json, -) -> Result> { +) -> Result> { println!(">> HANDLER: signup_handler called"); - match sign_up(&state.mongo_client, payload).await { - Ok(res) => Ok(res), + // check if the payload is empty + if payload.name.is_empty() + || payload.email.is_empty() + || payload.role.is_empty() + || payload.password.is_empty() + { + return Err(Error::InvalidPayload { + message: "Invalid payload".to_string(), + }); + } + + match Auth::sign_up( + &state.mongo_client, + &payload.name, + &payload.email, + &payload.role, + &payload.password, + ) + .await + { + Ok(res) => Ok(Json(res)), Err(e) => Err(e), } } @@ -22,11 +46,17 @@ pub async fn signup_handler( pub async fn signin_handler( State(state): State, payload: Json, -) -> Result> { +) -> Result> { println!(">> HANDLER: signin_handler called"); + // check if the payload is empty + if payload.email.is_empty() || payload.password.is_empty() { + return Err(Error::InvalidPayload { + message: "Invalid payload".to_string(), + }); + } - match sign_in(&state.mongo_client, payload).await { - Ok(res) => Ok(res), + match Auth::sign_in(&state.mongo_client, &payload.email, &payload.password).await { + Ok(res) => Ok(Json(res)), Err(e) => Err(e), } } @@ -44,12 +74,9 @@ pub async fn signout_handler( } match Session::revoke(&state.mongo_client, &payload.session_id).await { - Ok(_) => Ok(Json( - RevokeSessionsResult { - message: "Session revoked successfully".to_string(), - } - )), + Ok(_) => Ok(Json(RevokeSessionsResult { + message: "Session revoked successfully".to_string(), + })), Err(e) => Err(e), } - } diff --git a/src/handlers/user_handler.rs b/src/handlers/user_handler.rs index f56efd1..eb91b90 100644 --- a/src/handlers/user_handler.rs +++ b/src/handlers/user_handler.rs @@ -150,18 +150,25 @@ pub async fn get_user_email_handler( payload: Json, ) -> Result> { println!(">> HANDLER: get_user_by_email_handler called"); + if payload.email.is_empty() { + return Err(Error::InvalidPayload { + message: "Invalid payload".to_string(), + }); + } match User::get_from_email(&state.mongo_client, &payload.email).await { - Ok(user) => return Ok(Json(UserResponse { - uid: user.uid, - email: user.email, - name: user.name, - role: user.role, - is_active: user.is_active, - email_verified: user.email_verified, - created_at: user.created_at, - updated_at: user.updated_at, - })), + Ok(user) => { + return Ok(Json(UserResponse { + uid: user.uid, + email: user.email, + name: user.name, + role: user.role, + is_active: user.is_active, + email_verified: user.email_verified, + created_at: user.created_at, + updated_at: user.updated_at, + })) + } Err(e) => return Err(e), } } @@ -172,18 +179,25 @@ pub async fn get_user_id_handler( payload: Json, ) -> Result> { println!(">> HANDLER: get_user_by_id handler called"); + if payload.uid.is_empty() { + return Err(Error::InvalidPayload { + message: "Invalid payload".to_string(), + }); + } match User::get_from_uid(&state.mongo_client, &payload.uid).await { - Ok(user) => return Ok(Json(UserResponse { - uid: user.uid, - email: user.email, - name: user.name, - role: user.role, - is_active: user.is_active, - email_verified: user.email_verified, - created_at: user.created_at, - updated_at: user.updated_at, - })), + Ok(user) => { + return Ok(Json(UserResponse { + uid: user.uid, + email: user.email, + name: user.name, + role: user.role, + is_active: user.is_active, + email_verified: user.email_verified, + created_at: user.created_at, + updated_at: user.updated_at, + })) + } Err(e) => return Err(e), } } @@ -210,7 +224,7 @@ pub async fn delete_user_handler( return Ok(Json(UserEmailResponse { message: "User deleted".to_string(), email: payload.email.to_owned(), - })) + })); } Err(e) => return Err(e), } diff --git a/src/models/auth_model.rs b/src/models/auth_model.rs index 10ef7fa..f00a579 100644 --- a/src/models/auth_model.rs +++ b/src/models/auth_model.rs @@ -1,3 +1,4 @@ +use bson::DateTime; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Debug, Clone, Serialize)] @@ -8,8 +9,30 @@ pub struct SignUpPayload { pub role: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct SignInPayload { pub email: String, pub password: String, } + +#[derive(Debug, Deserialize, Serialize)] + +pub struct SessionResponseForSignInOrSignUp { + pub session_id: String, + pub id_token: String, + pub refresh_token: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SignInOrSignUpResponse { + pub message: String, + pub uid: String, + pub name: String, + pub email: String, + pub role: String, + pub created_at: Option, + pub updated_at: Option, + pub email_verified: bool, + pub is_active: bool, + pub session: SessionResponseForSignInOrSignUp, +} diff --git a/src/utils/auth_utils.rs b/src/utils/auth_utils.rs deleted file mode 100644 index b218afe..0000000 --- a/src/utils/auth_utils.rs +++ /dev/null @@ -1,139 +0,0 @@ -use axum::Json; -use bson::doc; -use mongodb::{Client, Collection}; -use serde_json::{json, Value}; - -use crate::{ - core::{dek::Dek, session::Session, user::User}, - errors::{Error, Result}, - models::auth_model::{SignInPayload, SignUpPayload}, - utils::{encryption_utils::Encryption, hashing_utils::verify_password_hash}, -}; - -pub async fn sign_up(mongo_client: &Client, payload: Json) -> Result> { - println!(">> HANDLER: add_user_handler called"); - // check if the payload is empty - if payload.name.is_empty() - || payload.email.is_empty() - || payload.role.is_empty() - || payload.password.is_empty() - { - return Err(Error::InvalidPayload { - message: "Invalid payload".to_string(), - }); - } - - let db = mongo_client.database("test"); - - let collection: Collection = db.collection("users"); - let cursor = collection - .find_one( - Some(doc! { - "email": payload.email.clone() - }), - None, - ) - .await - .unwrap(); - - if cursor.is_some() { - return Err(Error::UserAlreadyExists { - message: "User already exists".to_string(), - }); - } - - let dek = Dek::generate(); // create a data encryption key for new user - let user = match User::new( - &payload.name, - &payload.email, - &payload.role, - &payload.password, - ) - .encrypt_and_add(&mongo_client, &dek) - .await - { - Ok(user) => user, - Err(e) => return Err(e), - }; - - // add the dek to the deks collection - let dek_data = match Dek::new(&user.uid, &user.email, &dek) - .encrypt_and_add(&mongo_client) - .await - { - Ok(dek_data) => dek_data, - Err(e) => return Err(e), - }; - - let session = match Session::new(&user, "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36").encrypt_add(&mongo_client, &dek).await { - Ok(session) => session, - Err(e) => return Err(e), - }; - - Ok(Json(json!({ - "uid": user.uid, - "name": user.name, - "email": user.email, - "role": user.role, - "created_at": user.created_at, - "updated_at": user.updated_at, - "email_verified": user.email_verified, - "is_active": user.is_active, - "session": { - "session_id": Encryption::encrypt_data(&session.session_id, &dek_data.dek), - "id_token" : session.id_token, - "refresh_token" : session.refresh_token, - }, - }))) -} - -pub async fn sign_in(mongo_client: &Client, payload: Json) -> Result> { - // check if the payload is empty - if payload.email.is_empty() || payload.password.is_empty() { - return Err(Error::InvalidPayload { - message: "Invalid payload".to_string(), - }); - } - - let user = match User::get_from_email(&mongo_client, &payload.email).await { - Ok(user) => user, - Err(e) => return Err(e), - }; - - let dek_data = match Dek::get(&mongo_client, &user.uid).await { - Ok(dek_data) => dek_data, - Err(e) => return Err(e), - }; - - // verify the password - if verify_password_hash(&payload.password, &user.password) { - let session = match Session::new(&user, "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36").encrypt_add(&mongo_client, &dek_data.dek).await { - Ok(session) => session, - Err(e) => return Err(e), - }; - let res = Json(json!({ - "message": "Signin successful", - "user": { - "uid": user.uid, - "name": user.name, - "email": user.email, - "role": user.role, - "created_at": user.created_at, - "updated_at": user.updated_at, - "email_verified": user.email_verified, - "is_active": user.is_active, - "session": { - "session_id": Encryption::encrypt_data(&session.session_id, &dek_data.dek), - "id_token" : session.id_token, - "refresh_token" : session.refresh_token, - }, - }, - })); - - Ok(res) - } else { - Err(Error::UserNotFound { - message: "User not found".to_string(), - }) - } -} diff --git a/src/utils/hashing_utils.rs b/src/utils/hashing_utils.rs deleted file mode 100644 index dae4cf5..0000000 --- a/src/utils/hashing_utils.rs +++ /dev/null @@ -1,33 +0,0 @@ -use argon2::{ - password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, - Argon2, -}; -use sha256::digest; - -pub fn salt_and_hash_password(password: &str) -> String { - let input = String::from(password); - let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); - let password_hash_and_salted_with_argon = argon2 - .hash_password(input.as_bytes(), &salt) - .unwrap() - .to_string(); - let final_hash = digest(password_hash_and_salted_with_argon.to_string()); - // return the hashed password and the salt connected with . - return format!("{}.{}", final_hash, salt.to_string()) -} - -pub fn verify_password_hash(password: &str, hash: &str) -> bool { - let input = String::from(password); - // split the hash into hash and salt - let hash_salt: Vec<&str> = hash.split('.').collect(); - // convert the salt to SaltString - let salt_typed = SaltString::from_b64( hash_salt[1]).unwrap(); - let argon2 = Argon2::default(); - let password_hash_and_salted_with_argon = argon2 - .hash_password(input.as_bytes(), &salt_typed) - .unwrap() - .to_string(); - let final_hash = digest(password_hash_and_salted_with_argon.to_string()); - return final_hash == hash_salt[0]; -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 0e9fc42..9fb0e7c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,4 @@ -pub mod auth_utils; pub mod email_utils; pub mod encryption_utils; -pub mod hashing_utils; -pub mod session_utils; \ No newline at end of file +pub mod password_utils; +pub mod session_utils; diff --git a/src/utils/password_utils.rs b/src/utils/password_utils.rs new file mode 100644 index 0000000..470b417 --- /dev/null +++ b/src/utils/password_utils.rs @@ -0,0 +1,37 @@ +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, +}; +use sha256::digest; + +pub struct Password; + +impl Password { + pub fn salt_and_hash(password: &str) -> String { + let input = String::from(password); + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let password_hash_and_salted_with_argon = argon2 + .hash_password(input.as_bytes(), &salt) + .unwrap() + .to_string(); + let final_hash = digest(password_hash_and_salted_with_argon.to_string()); + // return the hashed password and the salt connected with . + return format!("{}.{}", final_hash, salt.to_string()); + } + + pub fn verify_hash(password: &str, hash: &str) -> bool { + let input = String::from(password); + // split the hash into hash and salt + let hash_salt: Vec<&str> = hash.split('.').collect(); + // convert the salt to SaltString + let salt_typed = SaltString::from_b64(hash_salt[1]).unwrap(); + let argon2 = Argon2::default(); + let password_hash_and_salted_with_argon = argon2 + .hash_password(input.as_bytes(), &salt_typed) + .unwrap() + .to_string(); + let final_hash = digest(password_hash_and_salted_with_argon.to_string()); + return final_hash == hash_salt[0]; + } +}