From a78aafcd80f0ab7a10725f4865495bee9c76f9bf Mon Sep 17 00:00:00 2001 From: Rajdeep Sengupta Date: Fri, 3 May 2024 21:01:27 +0530 Subject: [PATCH] Session started --- compose.yaml | 2 - src/core/session.rs | 202 ++++++++++++++++++++++++++++++-- src/core/user.rs | 4 +- src/errors.rs | 30 +++-- src/handlers/session_handler.rs | 37 ++++-- src/handlers/user_handler.rs | 10 +- src/main.rs | 2 +- src/routes/session_routes.rs | 13 +- src/utils/auth_utils.rs | 43 ++++--- src/utils/session_utils.rs | 119 ++++++++++++++----- 10 files changed, 373 insertions(+), 89 deletions(-) diff --git a/compose.yaml b/compose.yaml index bcc2616..2bbee94 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,5 +1,3 @@ -version: '3.9' - services: server: build: diff --git a/src/core/session.rs b/src/core/session.rs index f890f32..9458469 100644 --- a/src/core/session.rs +++ b/src/core/session.rs @@ -1,27 +1,46 @@ -use bson::DateTime; +use crate::{ + errors::{Error, Result}, + traits::{decryption::Decrypt, encryption::Encrypt}, + utils::{encryption_utils::Encryption, session_utils::{IDToken, RefreshToken}}, +}; +use bson::{doc, DateTime}; +use futures::StreamExt; use mongodb::{Client, Collection}; use serde::{Deserialize, Serialize}; -use crate ::{ - errors::{Error, Result}, traits::encryption::Encrypt -}; + +use super::{dek::Dek, user::User}; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Session{ +pub struct Session { pub uid: String, pub email: String, pub id_token: String, pub refresh_token: String, + pub user_agent: String, + pub is_active: bool, pub created_at: DateTime, pub updated_at: DateTime, } impl Session { - pub fn new(uid: String, email: String, id_token: String, refresh_token: String) -> Self { + pub fn new(user: &User, user_agent: &str) -> Self { + let id_token = match IDToken::new(user).sign() { + Ok(token) => token, + Err(_) => "".to_string(), + }; + + let refresh_token = match RefreshToken::new(&user.uid).sign() { + Ok(token) => token, + Err(_) => "".to_string(), + }; + Self { - uid, - email, + uid: user.uid.to_string(), + email: user.email.to_string(), id_token, refresh_token, + user_agent: user_agent.to_string(), + is_active: true, created_at: DateTime::now(), updated_at: DateTime::now(), } @@ -40,4 +59,169 @@ impl Session { }), } } -} \ No newline at end of file + + pub async fn verify( + mongo_client: &Client, + id_token: &str, + ) -> Result { + let token_data = match IDToken::verify(&id_token) { + Ok(token_data) => { + let db = mongo_client.database("test"); + let collection_session: Collection = db.collection("sessions"); + + let dek_data = match Dek::get(mongo_client, &token_data.uid).await { + Ok(dek) => dek, + Err(e) => return Err(e), + }; + + let encrypted_id = Encryption::encrypt_data(&token_data.uid, &dek_data.dek); + let encrypted_id_token = Encryption::encrypt_data(&id_token, &dek_data.dek); + + let session = match collection_session + .count_documents(doc! { + "uid": encrypted_id, + "id_token": encrypted_id_token, + "is_active": true, + }, None) + .await + { + Ok(count) => { + if count > 0 { + Ok(()) + } else { + Err(Error::SessionExpired { + message: "Invalid token".to_string(), + }) + } + }, + Err(e) => Err(Error::ServerError { + message: e.to_string(), + }), + }; + if session.is_err() { + return Err(Error::InvalidToken { + message: "Invalid token".to_string(), + }); + } else { + Ok(token_data) + } + }, + Err(e) => return Err(e), + }; + token_data + } + + pub async fn get_all_from_uid(mongo_client: &Client, uid: &str) -> Result> { + let db = mongo_client.database("test"); + let collection_session: Collection = db.collection("sessions"); + + let dek_data = match Dek::get(mongo_client, uid).await { + Ok(dek) => dek, + Err(e) => return Err(e), + }; + + let encrypted_uid = Encryption::encrypt_data(uid, &dek_data.dek); + + let mut cursor = collection_session + .find( + doc! { + "uid": encrypted_uid, + "is_active": true, + }, + None, + ) + .await + .unwrap(); + + let mut sessions: Vec = Vec::new(); + while let Some(session) = cursor.next().await { + match session { + Ok(data) => { + let decrypted_session = data.decrypt(&dek_data.dek); + sessions.push(decrypted_session); + } + Err(e) => return Err(Error::ServerError { message: e.to_string() }), + } + } + Ok(sessions) + } + + pub async fn revoke_all(mongo_client: &Client, uid: &str) -> Result<()> { + let db = mongo_client.database("test"); + let collection_session: Collection = db.collection("sessions"); + + match collection_session + .update_many( + doc! {"uid": uid}, + doc! {"$set": {"is_active": false}}, + None, + ) + .await + { + Ok(_) => Ok(()), + Err(e) => Err(Error::ServerError { + message: e.to_string(), + }), + } + } + + pub async fn revoke( + id_token: &str, + refresh_token: &str, + mongo_client: &Client, + ) -> Result<()> { + let db = mongo_client.database("test"); + let collection_session: Collection = db.collection("sessions"); + + match collection_session + .update_one( + doc! {"id_token": id_token, "refresh_token": refresh_token }, + doc! {"$set": {"is_active": false}}, + None, + ) + .await + { + Ok(_) => Ok(()), + Err(e) => Err(Error::ServerError { + message: e.to_string(), + }), + } + } + + pub async fn delete(id_token: &str, refresh_token: &str, mongo_client: &Client) -> Result<()> { + let db = mongo_client.database("test"); + let collection_session: Collection = db.collection("sessions"); + + match collection_session + .delete_one( + doc! {"id_token": id_token, "refresh_token": refresh_token }, + None, + ) + .await + { + Ok(_) => Ok(()), + Err(e) => Err(Error::ServerError { + message: e.to_string(), + }), + } + } + + + pub async fn delete_all(mongo_client: &Client, uid: &str) -> Result<()> { + let db = mongo_client.database("test"); + let collection_session: Collection = db.collection("sessions"); + + match collection_session + .delete_many( + doc! {"uid": uid}, + None, + ) + .await + { + Ok(_) => Ok(()), + Err(e) => Err(Error::ServerError { + message: e.to_string(), + }), + } + } +} diff --git a/src/core/user.rs b/src/core/user.rs index f86d4a2..fd3e351 100644 --- a/src/core/user.rs +++ b/src/core/user.rs @@ -504,7 +504,7 @@ impl User { Ok("Password updated successfully".to_string()) } - pub async fn delete(mongo_client: &Client, email: &str) -> Result<()> { + pub async fn delete(mongo_client: &Client, email: &str) -> Result { let db = mongo_client.database("test"); let collection: Collection = db.collection("users"); let collection_dek: Collection = db.collection("deks"); @@ -552,7 +552,7 @@ impl User { message: "DEK not found".to_string(), }); } - Ok(()) + Ok(dek_data.uid) } Err(_) => { return Err(Error::ServerError { diff --git a/src/errors.rs b/src/errors.rs index fc14e80..631b3a8 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -22,16 +22,19 @@ pub enum Error { // -- Session Errors InvalidToken { message: String }, - - ServerError { message: String }, - - // -- Encryption Errors - KeyNotFound { message: String }, + RefreshTokenCreationError { message: String }, + IdTokenCreationError { message: String }, PublicKeyLoadError { message: String }, PrivateKeyLoadError { message: String }, SignatureVerificationError { message: String }, ExpiredSignature { message: String }, - IdTokenCreationError { message: String }, + SessionExpired { message: String }, + + + // -- Encryption Errors + KeyNotFound { message: String }, + + ServerError { message: String }, } impl IntoResponse for Error { @@ -104,12 +107,22 @@ impl Error { Self::InvalidToken { message: _ } => { (StatusCode::UNAUTHORIZED, ClientError::INVALID_TOKEN) } + Self::IdTokenCreationError { message: _ } => ( + StatusCode::INTERNAL_SERVER_ERROR, + ClientError::SERVICE_ERROR, + ), - Self::ServerError { message: _ } => ( + Self::RefreshTokenCreationError { message: _ } => ( StatusCode::INTERNAL_SERVER_ERROR, ClientError::SERVICE_ERROR, ), - Self::IdTokenCreationError { message: _ } => ( + + Self::SessionExpired { message: _ } => ( + StatusCode::UNAUTHORIZED, + ClientError::SESSION_EXPIRED, + ), + + Self::ServerError { message: _ } => ( StatusCode::INTERNAL_SERVER_ERROR, ClientError::SERVICE_ERROR, ), @@ -134,6 +147,7 @@ pub enum ClientError { INVALID_TOKEN, SIGNATURE_VERIFICATION_ERROR, EXPIRED_SIGNATURE, + SESSION_EXPIRED } // region: --- Error Boilerplate diff --git a/src/handlers/session_handler.rs b/src/handlers/session_handler.rs index 33a7683..3bf8560 100644 --- a/src/handlers/session_handler.rs +++ b/src/handlers/session_handler.rs @@ -1,21 +1,44 @@ -use axum::Json; +use axum::{extract::State, Json}; use axum_macros::debug_handler; -use serde_json::{json, Value}; -use crate::{errors::{Error, Result}, models::session_model::VerifyJwt, utils::session_utils::IDToken}; +use crate::{core::session::Session, errors::{Error, Result}, models::{session_model::VerifyJwt, user_model::UserIdPayload}, utils::session_utils::IDToken, AppState}; #[debug_handler] -pub async fn verify_jwt_handler( +pub async fn verify_session( + State(state): State, payload: Json, -) -> Result> { +) -> Result> { // check if the token is not empty if payload.token.is_empty() { return Err(Error::InvalidPayload { message: "Invalid payload passed".to_string() }); } // verify the token - let _ = match IDToken::verify(&payload.token) { - Ok(val) => return Ok(Json(json!(val))), + match Session::verify(&state.mongo_client, &payload.token).await { + Ok(data) => { + return Ok(Json(data)); + } Err(e) => return Err(e), + }; } + +#[debug_handler] +pub async fn get_all_from_uid( + State(state): State, + payload: Json, +) -> Result>> { + // check if the token is not empty + if payload.uid.is_empty() { + return Err(Error::InvalidPayload { message: "Invalid payload passed".to_string() }); + } + + // verify the token + match Session::get_all_from_uid(&state.mongo_client, &payload.uid).await { + Ok(data) => { + return Ok(Json(data)); + } + Err(e) => return Err(e), + + }; +} \ No newline at end of file diff --git a/src/handlers/user_handler.rs b/src/handlers/user_handler.rs index 995026d..f56efd1 100644 --- a/src/handlers/user_handler.rs +++ b/src/handlers/user_handler.rs @@ -1,5 +1,5 @@ use crate::{ - core::{dek::Dek, user::User}, + core::{dek::Dek, session::Session, user::User}, errors::{Error, Result}, models::user_model::{ ToggleUserActivationStatusPayload, ToggleUserActivationStatusResponse, UpdateUserPayload, @@ -201,8 +201,12 @@ pub async fn delete_user_handler( }); } - match User::delete(&State(state).mongo_client, &payload.email).await { - Ok(_) => { + match User::delete(&State(&state).mongo_client, &payload.email).await { + Ok(uid) => { + match Session::delete_all(&State(&state).mongo_client, &uid).await { + Ok(_) => {} + Err(e) => return Err(e), + } return Ok(Json(UserEmailResponse { message: "User deleted".to_string(), email: payload.email.to_owned(), diff --git a/src/main.rs b/src/main.rs index 1d2e9a7..073ddd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,7 @@ async fn main() -> Result<(), Box> { .merge(routes::auth_routes::routes(State(app_state.clone()))) .merge(routes::user_routes::routes(State(app_state.clone()))) .merge(routes::password_routes::routes(State(app_state.clone()))) - .merge(routes::session_routes::routes()) + .merge(routes::session_routes::routes(State(app_state.clone()))) .layer(middleware::map_response(main_response_mapper)); let app = Router::new().nest("/api", routes); diff --git a/src/routes/session_routes.rs b/src/routes/session_routes.rs index 72c9573..eda413f 100644 --- a/src/routes/session_routes.rs +++ b/src/routes/session_routes.rs @@ -1,8 +1,11 @@ -use axum::{routing::post, Router}; +use axum::{extract::State, routing::post, Router}; -use crate::handlers::session_handler::verify_jwt_handler; +use crate::{handlers::session_handler::{verify_session, get_all_from_uid}, AppState}; -pub fn routes() -> Router { - Router::new() - .route("/verify-session", post(verify_jwt_handler)) +pub fn routes(State(state): State) -> Router { + let session_routes = Router::new() + .route("/verify", post(verify_session)) + .route("/get_all_from_uid", post(get_all_from_uid)); + + Router::new().nest("/session", session_routes).with_state(state) } \ No newline at end of file diff --git a/src/utils/auth_utils.rs b/src/utils/auth_utils.rs index a6f4a7a..814a64e 100644 --- a/src/utils/auth_utils.rs +++ b/src/utils/auth_utils.rs @@ -4,7 +4,7 @@ use mongodb::{Client, Collection}; use serde_json::{json, Value}; use crate::{ - core::{dek::Dek, user::User}, + core::{dek::Dek, session::Session, user::User}, errors::{Error, Result}, models::auth_model::{SignInPayload, SignUpPayload}, utils::{ @@ -67,15 +67,9 @@ pub async fn sign_up(mongo_client: &Client, payload: Json) -> Res Err(e) => return Err(e), }; - // issue a jwt token - let token = match IDToken::new(&user).sign() { - Ok(token) => token, - Err(err) => { - eprintln!("Error signing jwt token: {}", err); - return Err(Error::IdTokenCreationError { - message: err.to_string(), - }); - } + 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!({ @@ -87,7 +81,10 @@ pub async fn sign_up(mongo_client: &Client, payload: Json) -> Res "updated_at": user.updated_at, "email_verified": user.email_verified, "is_active": user.is_active, - "token": token, + "session": { + "id_token" : session.id_token, + "refresh_token" : session.refresh_token, + }, }))) } @@ -104,19 +101,16 @@ pub async fn sign_in(mongo_client: &Client, payload: Json) -> Res 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) { - // issue a jwt token - let token = match IDToken::new(&user) - .sign() - { - Ok(token) => token, - Err(err) => { - eprintln!("Error signing jwt token: {}", err); - return Err(Error::IdTokenCreationError { - message: err.to_string(), - }); - } + 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", @@ -129,7 +123,10 @@ pub async fn sign_in(mongo_client: &Client, payload: Json) -> Res "updated_at": user.updated_at, "email_verified": user.email_verified, "is_active": user.is_active, - "token": token, + "session": { + "id_token" : session.id_token, + "refresh_token" : session.refresh_token, + }, }, })); diff --git a/src/utils/session_utils.rs b/src/utils/session_utils.rs index ce50f7a..2018f7b 100644 --- a/src/utils/session_utils.rs +++ b/src/utils/session_utils.rs @@ -9,21 +9,12 @@ use crate::{core::user::User, errors::Error}; #[derive(Debug, Serialize, Deserialize)] pub struct IDToken { - uid: String, + pub uid: String, iss: String, iat: usize, exp: usize, token_type: String, - data: Option>, -} - -#[derive(Debug, Serialize, Deserialize)] -struct RefreshTokenClaims { - uid: String, - iss: String, - iat: usize, - exp: usize, - data: Option>, + pub data: Option>, } fn load_private_key() -> Result, Error> { @@ -146,23 +137,93 @@ impl IDToken { } } } -// pub async fn generate_refresh_token(user: &User) -> Result> { -// let secret = dotenv::var("JWT_SECRET").expect("JWT_SECRET must be set"); -// let server_url = env::var("SERVER_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); -// let header = Header::new(jwt::Algorithm::HS256); -// let token = jwt::encode( -// &header, -// &RefreshTokenClaims { -// uid: user.uid.clone(), -// iss: server_url, -// iat: chrono::Utc::now().timestamp() as usize, -// exp: chrono::Utc::now().timestamp() as usize + (3600 * 12), // 12h -// data: None, -// }, -// &EncodingKey::from_secret(secret.as_ref()), -// ) -// .unwrap(); +// RefreshToken struct +#[derive(Debug, Serialize, Deserialize)] +pub struct RefreshToken { + pub uid: String, + iss: String, + iat: usize, + exp: usize, + scope: String, + pub data: Option>, +} + +impl RefreshToken { + pub fn new(uid: &str) -> Self { + let server_url = + std::env::var("SERVER_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); + Self { + uid: uid.to_string(), + iss: server_url, + iat: chrono::Utc::now().timestamp() as usize, + exp: chrono::Utc::now().timestamp() as usize + (3600 * 24 * 45), // 45 days + scope: "get_new_id_token".to_string(), + data: None, + } + } + + pub fn sign(&self) -> Result { + let private_key = load_private_key()?; + let header = Header::new(jwt::Algorithm::RS256); + let encoding_key = match EncodingKey::from_rsa_pem(&private_key) { + Ok(key) => key, + Err(err) => { + eprintln!("Error creating decoding key: {}", err); + return Err(Error::PublicKeyLoadError { + message: (err.to_string()), + }); + } + }; + + match jwt::encode(&header, &self, &encoding_key) { + Ok(token) => return Ok(token), + Err(err) => { + return Err(Error::RefreshTokenCreationError { + message: err.to_string(), + }) + } + }; + } -// Ok(token) -// } + pub fn verify(token: &str) -> Result { + let public_key = load_public_key()?; + let validation = Validation::new(jwt::Algorithm::RS256); + // Try to create a DecodingKey from the public key + let decoding_key = match DecodingKey::from_rsa_pem(&public_key) { + Ok(key) => key, + Err(err) => { + eprintln!("Error creating decoding key: {}", err); + return Err(Error::PublicKeyLoadError { + message: (err.to_string()), + }); + } + }; + // return false if the token is not valid + match jwt::decode::(&token, &decoding_key, &validation) { + Ok(val) => { + let token_data = val.claims; + Ok(token_data) + } + Err(e) => match e { + // check if ExpiredSignature + _ if e.to_string().contains("ExpiredSignature") => { + return Err(Error::ExpiredSignature { + message: "Expired signature".to_string(), + }) + } + // check if InvalidSignature + _ if e.to_string().contains("InvalidSignature") => { + return Err(Error::SignatureVerificationError { + message: "Invalid signature".to_string(), + }) + } + _ => { + return Err(Error::InvalidToken { + message: "Invalid token".to_string(), + }) + } + }, + } + } +} \ No newline at end of file