├── src ├── traits │ ├── mod.rs │ ├── decryption.rs │ └── encryption.rs ├── config │ ├── mod.rs │ ├── db_connection_handler.rs │ └── init.rs ├── middlewares │ ├── mod.rs │ ├── with_api_key.rs │ └── res_log.rs ├── assets │ ├── logo.png │ └── logo.svg ├── core │ ├── mod.rs │ ├── dek.rs │ ├── auth.rs │ └── session.rs ├── models │ ├── mod.rs │ ├── overview_model.rs │ ├── password_model.rs │ ├── auth_model.rs │ ├── session_model.rs │ └── user_model.rs ├── utils │ ├── mod.rs │ ├── encryption_utils.rs │ ├── password_utils.rs │ ├── validation_utils.rs │ ├── email_utils.rs │ └── session_utils.rs ├── routes │ ├── mod.rs │ ├── health_check_routes.rs │ ├── overview_routes.rs │ ├── auth_routes.rs │ ├── password_routes.rs │ ├── session_routes.rs │ └── user_routes.rs ├── handlers │ ├── mod.rs │ ├── health_check_handler.rs │ ├── overview_handler.rs │ ├── auth_handler.rs │ ├── session_handler.rs │ ├── password_handler.rs │ └── user_handler.rs ├── cli │ └── main.rs ├── main.rs └── errors.rs ├── .gitignore ├── k8s └── local │ ├── flexauth-config-map.yaml │ ├── mongodb-depl.yaml │ ├── smtp-depl.yaml │ ├── mongodb-express-depl.yaml │ └── flexauth-service-depl.yaml ├── .vscode └── settings.json ├── .github └── workflows │ └── rust.yml ├── skaffold.template.yaml ├── docs ├── backend │ ├── email-system.md │ ├── user-data-protection.md │ ├── password-protection.md │ └── session-managment.md ├── folder-structure │ └── readme.md └── local-setup │ └── readme.md ├── smtp ├── Dockerfile └── main.cf ├── .dockerignore ├── Cargo.toml ├── LICENSE ├── docker-compose.yaml ├── Dockerfile ├── README.md └── makefile /src/traits/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod encryption; 2 | pub mod decryption; -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db_connection_handler; 2 | pub mod init; -------------------------------------------------------------------------------- /src/middlewares/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod res_log; 2 | pub mod with_api_key; -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rajdip019/flexauth/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod dek; 3 | pub mod session; 4 | pub mod user; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | data 3 | .env 4 | .env.local 5 | private_key.pem 6 | .errorviz-version 7 | skaffold.generated.yaml -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth_model; 2 | pub mod overview_model; 3 | pub mod password_model; 4 | pub mod session_model; 5 | pub mod user_model; 6 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod email_utils; 2 | pub mod encryption_utils; 3 | pub mod password_utils; 4 | pub mod session_utils; 5 | pub mod validation_utils; 6 | -------------------------------------------------------------------------------- /k8s/local/flexauth-config-map.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: flexauth-config 5 | namespace: flexauth 6 | data: 7 | database_url: mongodb-service -------------------------------------------------------------------------------- /src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth_routes; 2 | pub mod health_check_routes; 3 | pub mod overview_routes; 4 | pub mod password_routes; 5 | pub mod session_routes; 6 | pub mod user_routes; 7 | -------------------------------------------------------------------------------- /src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth_handler; 2 | pub mod health_check_handler; 3 | pub mod overview_handler; 4 | pub mod password_handler; 5 | pub mod session_handler; 6 | pub mod user_handler; 7 | -------------------------------------------------------------------------------- /src/handlers/health_check_handler.rs: -------------------------------------------------------------------------------- 1 | use axum::Json; 2 | use serde_json::{json, Value}; 3 | use crate::errors::Result; 4 | 5 | pub async fn health_check_handler() -> Result> { 6 | Ok(Json(json!({"status": "ok"}))) 7 | } -------------------------------------------------------------------------------- /src/routes/health_check_routes.rs: -------------------------------------------------------------------------------- 1 | use axum::{routing::get, Router}; 2 | 3 | use crate::handlers::health_check_handler::health_check_handler; 4 | 5 | pub fn routes() -> Router { 6 | Router::new() 7 | .route("/health", get(health_check_handler)) 8 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | "./Cargo.toml", 4 | ], 5 | "cSpell.words": [ 6 | "blazingly", 7 | "bson", 8 | "chrono", 9 | "deks", 10 | "dotenv", 11 | "flexauth", 12 | "inhouse", 13 | "jsonwebtoken", 14 | "lettre", 15 | "signin", 16 | "signout" 17 | ] 18 | } -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Build 19 | run: cargo build --verbose 20 | - name: Run tests 21 | run: cargo test --verbose 22 | -------------------------------------------------------------------------------- /src/routes/overview_routes.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::State, routing::get, Router}; 2 | 3 | use crate::{handlers::overview_handler::get_all_overview_handler, AppState}; 4 | 5 | pub fn routes(State(state): State) -> Router { 6 | let overview_routes = Router::new().route("/get-all", get(get_all_overview_handler)); 7 | 8 | Router::new() 9 | .nest("/overview", overview_routes) 10 | .with_state(state) 11 | } 12 | -------------------------------------------------------------------------------- /src/models/overview_model.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct OverviewResponse { 5 | pub user_count: usize, 6 | pub active_user_count: usize, 7 | pub inactive_user_count: usize, 8 | pub blocked_user_count: usize, 9 | pub revoked_session_count: usize, 10 | pub active_session_count: usize, 11 | pub os_types: Vec, 12 | pub device_types: Vec, 13 | pub browser_types: Vec, 14 | } 15 | -------------------------------------------------------------------------------- /src/routes/auth_routes.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::State, routing::post, Router}; 2 | 3 | use crate::{ 4 | handlers::auth_handler::{signin_handler, signout_handler, signup_handler}, AppState 5 | }; 6 | 7 | pub fn routes(State(state): State) -> Router { 8 | let auth_routes = Router::new() 9 | .route("/signup", post(signup_handler)) 10 | .route("/signin", post(signin_handler)) 11 | .route("/signout", post(signout_handler)); 12 | 13 | Router::new().nest("/auth", auth_routes).with_state(state) 14 | } 15 | -------------------------------------------------------------------------------- /src/routes/password_routes.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::State, routing::post, Router}; 2 | 3 | use crate::{handlers::password_handler::{forget_password_request_handler, forget_password_reset_handler, reset_password_handler}, AppState}; 4 | 5 | pub fn routes(State(state): State) -> Router { 6 | let password_rotes = Router::new() 7 | .route("/reset", post(reset_password_handler)) 8 | .route("/forget-request", post(forget_password_request_handler)) 9 | .route("/forget-reset/:id", post(forget_password_reset_handler)); 10 | 11 | 12 | Router::new().nest("/password", password_rotes).with_state(state) 13 | } -------------------------------------------------------------------------------- /skaffold.template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta11 2 | kind: Config 3 | 4 | build: 5 | artifacts: 6 | - image: flexauth-server 7 | context: . 8 | docker: 9 | dockerfile: Dockerfile 10 | target: dev 11 | - image: smtp-server 12 | context: . 13 | docker: 14 | dockerfile: Dockerfile.smtp 15 | target: smtp 16 | buildArgs: 17 | EMAIL: "${EMAIL}" 18 | EMAIL_PASSWORD: "${EMAIL_PASSWORD}" 19 | MAIL_NAME: "${MAIL_NAME}" 20 | SMTP_DOMAIN: "${SMTP_DOMAIN}" 21 | SMTP_PORT: "${SMTP_PORT}" 22 | 23 | deploy: 24 | kubectl: 25 | manifests: 26 | - k8s/local/* 27 | -------------------------------------------------------------------------------- /docs/backend/email-system.md: -------------------------------------------------------------------------------- 1 | # Emails 2 | 3 | When you start the local server with `docker` or other ways a local SMTP server starts. Generally, we use Gmail / other provider keys in the SMTP server to send emails using the provided email id. 4 | 5 | To know more on how to get the Keys for local setup. Check the Local setup readme. [Lcoal Setup Readme](https://github.com/Rajdip019/in-house-auth/blob/main/README.md) 6 | 7 | 8 | ## Diagram 9 | 10 | ![email-inhouse-auth](https://github.com/Rajdip019/in-house-auth/assets/91758830/7ea341de-f486-42bc-9070-5b9163dbc7e4) 11 | 12 | 13 | ## Feedback 14 | 15 | If you have any feedback, please raise an issue or start a discussion. Thank you. 16 | 17 | -------------------------------------------------------------------------------- /smtp/Dockerfile: -------------------------------------------------------------------------------- 1 | # Docker File for SMTP Server 2 | FROM ubuntu:latest AS smtp 3 | 4 | ARG EMAIL 5 | ARG EMAIL_PASSWORD 6 | ARG MAIL_NAME 7 | ARG SMTP_DOMAIN 8 | ARG SMTP_PORT 9 | 10 | RUN apt-get update && \ 11 | apt-get install -y mailutils && \ 12 | apt install -y postfix 13 | 14 | COPY /main.cf /etc/postfix/main.cf 15 | 16 | RUN sh -c 'echo "root: ${EMAIL}" >> /etc/aliases' && \ 17 | sh -c 'echo "${MAIL_NAME}" >> /etc/mailname' && \ 18 | sh -c 'echo "[${SMTP_DOMAIN}]:${SMTP_PORT} ${EMAIL}:${EMAIL_PASSWORD}" >> /etc/postfix/sasl_passwd' && \ 19 | postmap /etc/postfix/sasl_passwd && \ 20 | chmod 0600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db 21 | 22 | CMD service postfix restart && tail -f /dev/null -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/go/build-context-dockerignore/ 6 | 7 | **/.DS_Store 8 | **/.classpath 9 | **/.dockerignore 10 | **/.env 11 | **/.git 12 | **/.gitignore 13 | **/.project 14 | **/.settings 15 | **/.toolstarget 16 | **/.vs 17 | **/.vscode 18 | **/*.*proj.user 19 | **/*.dbmdl 20 | **/*.jfm 21 | **/charts 22 | **/docker-compose* 23 | **/compose* 24 | **/Dockerfile* 25 | **/node_modules 26 | **/npm-debug.log 27 | **/secrets.dev.yaml 28 | **/values.dev.yaml 29 | /bin 30 | /target 31 | /ui 32 | LICENSE 33 | README.md 34 | -------------------------------------------------------------------------------- /docs/folder-structure/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## Folder Structure Explanation 3 | 4 | - **src/**: This directory contains the source code of the project. 5 | - **cli/**: All the logic for the CLI executable functions resides here. 6 | - **config/**: Configuration files such as db connection configurations. 7 | - **routes/**: Route definitions for the API endpoints. 8 | - **utils/**: Utility functions or helper modules. 9 | - **core/**: Has all the logics for the core modules such as `Auth`,`Session`. 10 | - **handlers/**: Contains all the API Handlers. 11 | - **middlewares/**: Contains all the middleware functions 12 | - **models/**: Contains all the Data Models 13 | 14 | - **docs/**: Documentation files are stored here. 15 | 16 | Feel free to adjust this structure according to your project's needs and conventions. 17 | -------------------------------------------------------------------------------- /src/cli/main.rs: -------------------------------------------------------------------------------- 1 | use aes_gcm::{aead::OsRng, AeadCore, Aes256Gcm, KeyInit}; 2 | 3 | pub fn create_kek() -> String { 4 | let key = Aes256Gcm::generate_key(OsRng); 5 | // convert the key to hex string 6 | let hex_key = key.iter().map(|b| format!("{:02x}", b)).collect::().chars().take(32).collect::(); 7 | let iv = Aes256Gcm::generate_nonce(&mut OsRng); 8 | // convert the iv to hex string 9 | let hex_iv = iv.iter().map(|b| format!("{:02x}", b)).collect::().chars().take(12).collect::(); 10 | // connect the key and iv with . between them 11 | let key_iv = format!("{}.{}", hex_key, hex_iv); 12 | return key_iv; 13 | } 14 | 15 | pub fn generate_key_encryption_key() -> String { 16 | let key = create_kek(); 17 | key 18 | } 19 | 20 | fn main() { 21 | let key = generate_key_encryption_key(); 22 | println!("Key Encryption Key: {}", key) 23 | } -------------------------------------------------------------------------------- /src/models/password_model.rs: -------------------------------------------------------------------------------- 1 | use bson::{oid::ObjectId, DateTime}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Deserialize, Debug, Clone, Serialize)] 5 | pub struct ForgetPasswordRequest { 6 | pub _id: ObjectId, 7 | pub email: String, 8 | pub req_id: String, 9 | pub is_used: bool, 10 | pub valid_till: DateTime, 11 | pub created_at: DateTime, 12 | pub updated_at: DateTime, 13 | } 14 | 15 | #[derive(Deserialize, Debug, Clone, Serialize)] 16 | pub struct ResetPasswordPayload { 17 | pub email: String, 18 | pub old_password: String, 19 | pub new_password: String, 20 | } 21 | 22 | #[derive(Deserialize, Debug, Clone, Serialize)] 23 | pub struct ForgetPasswordResetPayload { 24 | pub email: String, 25 | pub password: String, 26 | } 27 | 28 | #[derive(Serialize, Deserialize, Debug, Clone)] 29 | pub struct ForgetPasswordPayload { 30 | pub email: String, 31 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "inhouse-auth" 3 | version = "0.1.0" 4 | edition = "2021" 5 | default-run = "inhouse-auth" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | [[bin]] 9 | name="create_kek" 10 | path="src/cli/main.rs" 11 | 12 | 13 | [dependencies] 14 | mongodb = "2.1" 15 | bson = { version = "2", features = ["chrono-0_4"] } # Needed for using chrono datetime in doc 16 | tokio = "1" 17 | chrono = "0.4" # Used for setting DateTimes 18 | serde = "1" # Used in the Map Data into Structs section 19 | serde_json = "1.0.114" 20 | serde_with = "3.6.1" 21 | axum = "0.7.5" 22 | tower-cookies = "0.10.0" 23 | tower-http = { version = "0.5", features = ["fs"] } 24 | axum-macros = "0.4.1" 25 | strum_macros = "0.26.2" 26 | futures = "0.3.30" 27 | sha256 = "1.5.0" 28 | argon2 = "0.5.3" 29 | aes-gcm = "0.10.3" 30 | hex = "0.4.3" 31 | dotenv = "0.15.0" 32 | lettre = "0.11" 33 | jsonwebtoken = "9.3.0" 34 | openssl = "0.10.64" 35 | regex = "1.10.4" 36 | uuid = "1.8.0" 37 | woothee = "0.13.0" 38 | -------------------------------------------------------------------------------- /src/models/auth_model.rs: -------------------------------------------------------------------------------- 1 | use bson::DateTime; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Deserialize, Debug, Clone, Serialize)] 5 | pub struct SignUpPayload { 6 | pub name: String, 7 | pub email: String, 8 | pub password: String, 9 | pub role: String, 10 | } 11 | 12 | #[derive(Debug, Deserialize, Serialize, Clone)] 13 | pub struct SignInPayload { 14 | pub email: String, 15 | pub password: String, 16 | } 17 | 18 | #[derive(Debug, Deserialize, Serialize)] 19 | 20 | pub struct SessionResponseForSignInOrSignUp { 21 | pub session_id: String, 22 | pub id_token: String, 23 | pub refresh_token: String, 24 | } 25 | 26 | #[derive(Debug, Deserialize, Serialize)] 27 | pub struct SignInOrSignUpResponse { 28 | pub message: String, 29 | pub uid: String, 30 | pub name: String, 31 | pub email: String, 32 | pub role: String, 33 | pub created_at: Option, 34 | pub updated_at: Option, 35 | pub email_verified: bool, 36 | pub is_active: bool, 37 | pub session: SessionResponseForSignInOrSignUp, 38 | } 39 | -------------------------------------------------------------------------------- /src/routes/session_routes.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::State, routing::{get, post}, Router}; 2 | 3 | use crate::{ 4 | handlers::session_handler::{ 5 | delete_all_handler, delete_handler, get_all_from_uid_handler, get_all_handler, get_details_handler, refresh_session_handler, revoke_all_handler, revoke_handler, verify_session_handler 6 | }, AppState 7 | }; 8 | 9 | pub fn routes(State(state): State) -> Router { 10 | let session_routes = Router::new() 11 | .route("/verify", post(verify_session_handler)) 12 | .route("/get-all", get(get_all_handler)) 13 | .route("/get-all-from-uid", post(get_all_from_uid_handler)) 14 | .route("/get-details", post(get_details_handler)) 15 | .route("/refresh-session", post(refresh_session_handler)) 16 | .route("/revoke", post(revoke_handler)) 17 | .route("/revoke-all", post(revoke_all_handler)) 18 | .route("/delete", post(delete_handler)) 19 | .route("/delete-all", post(delete_all_handler)); 20 | 21 | Router::new() 22 | .nest("/session", session_routes) 23 | .with_state(state) 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rajdeep Sengupta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /k8s/local/mongodb-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: mongodb 5 | namespace: flexauth 6 | labels: 7 | app: mongodb 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: mongodb 13 | template: 14 | metadata: 15 | labels: 16 | app: mongodb 17 | spec: 18 | containers: 19 | - name: mongodb 20 | image: mongo 21 | ports: 22 | - containerPort: 27017 23 | env: 24 | - name: MONGO_INITDB_ROOT_USERNAME 25 | valueFrom: 26 | secretKeyRef: 27 | name: flexauth-secrets 28 | key: MONGO_INITDB_ROOT_USERNAME 29 | - name: MONGO_INITDB_ROOT_PASSWORD 30 | valueFrom: 31 | secretKeyRef: 32 | name: flexauth-secrets 33 | key: MONGO_INITDB_ROOT_PASSWORD 34 | --- 35 | apiVersion: v1 36 | kind: Service 37 | metadata: 38 | name: mongodb-service 39 | namespace: flexauth 40 | spec: 41 | selector: 42 | app: mongodb 43 | ports: 44 | - protocol: TCP 45 | port: 27017 46 | targetPort: 27017 47 | -------------------------------------------------------------------------------- /src/utils/encryption_utils.rs: -------------------------------------------------------------------------------- 1 | use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit}; 2 | 3 | pub struct Encryption; 4 | 5 | impl Encryption { 6 | pub fn encrypt_data(data: &str, key_iv: &str) -> String { 7 | // split the key_iv into key and iv 8 | let key_iv_vec: Vec<&str> = key_iv.split('.').collect(); 9 | let key_buff = Key::::from_slice(key_iv_vec[0].as_bytes().try_into().unwrap()); 10 | let cipher = Aes256Gcm::new(key_buff); 11 | let cipher_text = cipher 12 | .encrypt(key_iv_vec[1].as_bytes().into(), data.as_ref()) 13 | .unwrap(); 14 | // convert the cipher_text to string 15 | return hex::encode(cipher_text); 16 | } 17 | 18 | pub fn decrypt_data(cipher_text: &str, key_iv: &str) -> String { 19 | // convert the cipher_text to bytes 20 | let cipher_text = hex::decode(cipher_text).unwrap(); 21 | // split the key_iv into key and iv 22 | let key_iv_vec: Vec<&str> = key_iv.split('.').collect(); 23 | let key_buff = Key::::from_slice(key_iv_vec[0].as_bytes().try_into().unwrap()); 24 | let cipher = Aes256Gcm::new(key_buff); 25 | let data = cipher 26 | .decrypt(key_iv_vec[1].as_bytes().into(), cipher_text.as_ref()) 27 | .unwrap(); 28 | return String::from_utf8(data).unwrap(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/routes/user_routes.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::State, routing::{get, post}, Router 3 | }; 4 | 5 | use crate::{ 6 | handlers::user_handler::{ 7 | block_user_handler, delete_user_handler, get_all_users_handler, get_recent_users_handler, get_user_email_handler, get_user_id_handler, toggle_user_activation_status, update_user_handler, update_user_role_handler, verify_email_handler, verify_email_request_handler 8 | }, AppState 9 | }; 10 | 11 | pub fn routes(State(state): State) -> Router { 12 | let user_routes = Router::new() 13 | .route("/get-all", get(get_all_users_handler)) 14 | .route("/get-recent", post(get_recent_users_handler)) 15 | .route("/get-from-email", post(get_user_email_handler)) 16 | .route("/get-from-id", post(get_user_id_handler)) 17 | .route("/update", post(update_user_handler)) 18 | .route("/verify-email-request", post(verify_email_request_handler)) 19 | .route("/verify-email/:id", get(verify_email_handler)) 20 | .route("/block/:id", get(block_user_handler)) 21 | .route( 22 | "/toggle-account-active-status", 23 | post(toggle_user_activation_status), 24 | ) 25 | .route("/update-role", post(update_user_role_handler)) 26 | .route("/delete", post(delete_user_handler)); 27 | 28 | Router::new().nest("/user", user_routes).with_state(state) 29 | } 30 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | server: 3 | build: 4 | context: . 5 | target: dev 6 | ports: 7 | - "${PORT}:${PORT}" 8 | depends_on: 9 | - mongodb 10 | links: 11 | - mongodb 12 | - smtp-server 13 | environment: 14 | MONGO_URI: mongodb 15 | SERVER_KEK: ${SERVER_KEK} 16 | EMAIL: ${EMAIL} 17 | EMAIL_PASSWORD: ${EMAIL_PASSWORD} 18 | MAIL_NAME: ${MAIL_NAME} 19 | SMTP_DOMAIN: ${SMTP_DOMAIN} 20 | SMTP_PORT: ${SMTP_PORT} 21 | X_API_KEY: ${X_API_KEY} 22 | MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME} 23 | MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD} 24 | volumes: 25 | - ./src:/app/src 26 | 27 | 28 | mongodb: 29 | image: mongodb/mongodb-community-server:latest 30 | ports: 31 | - "27017:27017" 32 | environment: 33 | MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME} 34 | MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD} 35 | volumes: 36 | - ./data:/data/mongodb 37 | command: mongod --quiet --logpath /dev/null 38 | 39 | smtp-server: 40 | build: 41 | context: ./smtp 42 | target: smtp 43 | args: 44 | EMAIL: ${EMAIL} 45 | EMAIL_PASSWORD: ${EMAIL_PASSWORD} 46 | MAIL_NAME: ${MAIL_NAME} 47 | SMTP_DOMAIN: ${SMTP_DOMAIN} 48 | SMTP_PORT: ${SMTP_PORT} 49 | ports: 50 | - 25:25 51 | 52 | -------------------------------------------------------------------------------- /src/utils/password_utils.rs: -------------------------------------------------------------------------------- 1 | use argon2::{ 2 | password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, 3 | Argon2, 4 | }; 5 | use sha256::digest; 6 | 7 | pub struct Password; 8 | 9 | impl Password { 10 | pub fn salt_and_hash(password: &str) -> String { 11 | let input = String::from(password); 12 | let salt = SaltString::generate(&mut OsRng); 13 | let argon2 = Argon2::default(); 14 | let password_hash_and_salted_with_argon = argon2 15 | .hash_password(input.as_bytes(), &salt) 16 | .unwrap() 17 | .to_string(); 18 | let final_hash = digest(password_hash_and_salted_with_argon.to_string()); 19 | // return the hashed password and the salt connected with . 20 | return format!("{}.{}", final_hash, salt.to_string()); 21 | } 22 | 23 | pub fn verify_hash(password: &str, hash: &str) -> bool { 24 | let input = String::from(password); 25 | // split the hash into hash and salt 26 | let hash_salt: Vec<&str> = hash.split('.').collect(); 27 | // convert the salt to SaltString 28 | let salt_typed = SaltString::from_b64(hash_salt[1]).unwrap(); 29 | let argon2 = Argon2::default(); 30 | let password_hash_and_salted_with_argon = argon2 31 | .hash_password(input.as_bytes(), &salt_typed) 32 | .unwrap() 33 | .to_string(); 34 | let final_hash = digest(password_hash_and_salted_with_argon.to_string()); 35 | return final_hash == hash_salt[0]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/validation_utils.rs: -------------------------------------------------------------------------------- 1 | pub struct Validation; 2 | 3 | impl Validation { 4 | pub fn email(email: &str) -> bool { 5 | // Check if email is valid 6 | let re = regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap(); 7 | re.is_match(email) 8 | } 9 | 10 | pub fn password(password: &str) -> bool { 11 | // Minimum length requirement 12 | let min_length = 8; 13 | if password.len() < min_length { 14 | return false; 15 | } 16 | 17 | // Check for at least one lowercase letter 18 | let has_lowercase = password.chars().any(|c| c.is_lowercase()); 19 | if !has_lowercase { 20 | return false; 21 | } 22 | 23 | // Check for at least one uppercase letter 24 | let has_uppercase = password.chars().any(|c| c.is_uppercase()); 25 | if !has_uppercase { 26 | return false; 27 | } 28 | 29 | // Check for at least one number 30 | let has_number = password.chars().any(|c| c.is_numeric()); 31 | if !has_number { 32 | return false; 33 | } 34 | 35 | // Check for at least one special character 36 | let has_special = password.chars().any(|c| c.is_ascii_punctuation()); 37 | if !has_special { 38 | return false; 39 | } 40 | 41 | // No whitespace allowed 42 | if password.contains(' ') { 43 | return false; 44 | } 45 | 46 | // Password is valid 47 | return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /k8s/local/smtp-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: smtp-server 5 | namespace: flexauth 6 | labels: 7 | app: smtp-server 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: smtp-server 13 | template: 14 | metadata: 15 | labels: 16 | app: smtp-server 17 | spec: 18 | containers: 19 | - name: smtp-server 20 | image: smtp-server:latest 21 | imagePullPolicy: IfNotPresent 22 | ports: 23 | - containerPort: 8082 24 | env: 25 | - name: SMTP_PORT 26 | valueFrom: 27 | secretKeyRef: 28 | name: flexauth-secrets 29 | key: SMTP_PORT 30 | - name: SMTP_DOMAIN 31 | valueFrom: 32 | secretKeyRef: 33 | name: flexauth-secrets 34 | key: SMTP_DOMAIN 35 | - name: EMAIL 36 | valueFrom: 37 | secretKeyRef: 38 | name: flexauth-secrets 39 | key: EMAIL 40 | - name: EMAIL_PASSWORD 41 | valueFrom: 42 | secretKeyRef: 43 | name: flexauth-secrets 44 | key: EMAIL_PASSWORD 45 | 46 | --- 47 | 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: smtp-server-service 52 | namespace: flexauth 53 | spec: 54 | selector: 55 | app: smtp-server 56 | type: LoadBalancer 57 | ports: 58 | - protocol: TCP 59 | port: 8082 60 | targetPort: 8082 -------------------------------------------------------------------------------- /docs/backend/user-data-protection.md: -------------------------------------------------------------------------------- 1 | 2 | # User Data Protection 3 | 4 | Data protection is one of the main things for a auth server and we have taken that seriously. Here is a brief on how the data gets encrypted and stored in the database. 5 | 6 | 7 | 8 | ## Method 9 | 10 | The method we are using for encryption is **Envelope Encryption** 11 | 12 | ### Terminology ( to keep in mind ) 13 | - `DEK`: Data Encryption Key 14 | - `KEK`: Key Encryption Key 15 | 16 | 17 | ## Diagram 18 | 19 | ![data-protection-inhouse-auth](https://github.com/Rajdip019/in-house-auth/assets/91758830/163fdd5a-1757-481c-ba18-3a4bfacb72d2) 20 | 21 | 22 | ## Explaination 23 | 24 | Here is a step-by-step guide on how it works. 25 | 26 | ### Step 1: 27 | Every user is assigned a new and unique `DEK` when they sign up. 28 | 29 | ### Step 2: 30 | We encrypt all the user data from `Session Details`, `Password Reset Request`, and all with the user `DEK` using the `AESGcm256` algorithm and store it in DB. 31 | 32 | ### Step 3: 33 | The auth server has its own `KEK`. This is unique for the server. You can generate it by running the command below from the root of your project. ( Make sure you have cargo installed ) - [How to install cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) 34 | ``` 35 | cargo run --bin create_kek 36 | ``` 37 | 38 | ### Step 4: 39 | We use the `KEK` to encrypt the `DEK` using the same `AESGcm256` algorithm and store it in DB. 40 | 41 | ### Step 5: ( Additional ) 42 | For additional safety, you can use `GCP KMS`, `AWS KMS` or any other cloud provider for additional safety. 43 | 44 | 45 | 46 | ## Feedback 47 | 48 | If you have any feedback, please raise an issue or start a discussion. Thank you. 49 | -------------------------------------------------------------------------------- /src/config/db_connection_handler.rs: -------------------------------------------------------------------------------- 1 | use mongodb::{ 2 | options::{ClientOptions, ResolverConfig}, 3 | Client, 4 | }; 5 | use std::env; 6 | use std::error::Error; 7 | 8 | pub async fn connect() -> Result> { 9 | // Load the MongoDB connection string from an environment variable: 10 | let client_uri_main = env::var("MONGO_URI").unwrap_or("localhost".to_string()); 11 | 12 | let client_uri = format!( 13 | "mongodb://{}:{}@{}:27017/?directConnection=true&retryWrites=true&w=majority", 14 | env::var("MONGO_INITDB_ROOT_USERNAME").expect("MONGO_INITDB_ROOT_USERNAME required"), 15 | env::var("MONGO_INITDB_ROOT_PASSWORD").expect("MONGO_INITDB_ROOT_PASSWORD required"), 16 | client_uri_main 17 | ); 18 | 19 | let options = 20 | match ClientOptions::parse_with_resolver_config(&client_uri, ResolverConfig::cloudflare()) 21 | .await 22 | { 23 | Ok(options) => options, 24 | Err(e) => { 25 | eprintln!(">> Error parsing client options: {:?}", e); 26 | std::process::exit(1); 27 | } 28 | }; 29 | 30 | let client = Client::with_options(options.clone())?; 31 | // Print success message if the connection is successful or an error message if it fails: 32 | // test the connection to the database 33 | match client.list_database_names(None, None).await { 34 | Ok(_) => { 35 | println!(">> Successfully connected to the database"); 36 | Ok(client) 37 | } 38 | Err(e) => { 39 | eprintln!(">> Error connecting to the database: {:?}", e); 40 | std::process::exit(1); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /smtp/main.cf: -------------------------------------------------------------------------------- 1 | # See /usr/share/postfix/main.cf.dist for a commented, more complete version 2 | # Debian specific: Specifying a file name will cause the first 3 | # line of that file to be used as the name. The Debian default 4 | # is /etc/mailname. 5 | #myorigin = /etc/mailname 6 | smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu) 7 | biff = no 8 | # appending .domain is the MUA's job. 9 | append_dot_mydomain = no 10 | # Uncomment the next line to generate "delayed mail" warnings 11 | #delay_warning_time = 4h 12 | readme_directory = no 13 | # See http://www.postfix.org/COMPATIBILITY_README.html - default to 3.6 on 14 | # fresh installs. 15 | compatibility_level = 3.6 16 | # TLS parameters 17 | smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem 18 | smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key 19 | smtpd_tls_security_level=may 20 | smtp_tls_CApath=/etc/ssl/certs 21 | smtp_tls_security_level=may 22 | smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache 23 | smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination 24 | myhostname = localhost 25 | alias_maps = hash:/etc/aliases 26 | alias_database = hash:/etc/aliases 27 | myorigin = /etc/mailname 28 | mydestination = $myhostname, /etc/mailname, localhost, localhost.localdomain, localhost 29 | relayhost = [smtp.gmail.com]:587 30 | mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 31 | mailbox_size_limit = 0 32 | recipient_delimiter = + 33 | inet_interfaces = loopback-only 34 | inet_protocols = all 35 | smtp_sasl_auth_enable = yes 36 | smtp_sasl_security_options = noanonymous 37 | smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd 38 | smtp_use_tls = yes 39 | smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt -------------------------------------------------------------------------------- /src/traits/decryption.rs: -------------------------------------------------------------------------------- 1 | use serde::{de::DeserializeOwned, Serialize}; 2 | use serde_json::Value; 3 | 4 | use crate::utils::encryption_utils::Encryption; 5 | 6 | pub trait Decrypt { 7 | fn decrypt(&self, key: &str) -> Self; 8 | } 9 | 10 | impl Decrypt for T 11 | where 12 | T: Serialize + DeserializeOwned, 13 | { 14 | fn decrypt(&self, key: &str) -> Self { 15 | // Serialize the object to JSON 16 | let json_str = serde_json::to_string(self).unwrap(); 17 | 18 | // Deserialize the JSON string into a serde_json::Value 19 | let mut value: Value = serde_json::from_str(&json_str).unwrap(); 20 | 21 | // Decrypt the keys and values recursively 22 | decrypt_value(&mut value, key); 23 | 24 | // Deserialize the serde_json::Value back to the original object 25 | serde_json::from_value(value).unwrap() 26 | } 27 | } 28 | 29 | // Recursive function to decrypt object keys and values 30 | fn decrypt_value(value: &mut Value, key: &str) { 31 | match value { 32 | Value::String(s) => { 33 | // Decrypt string values 34 | *s = Encryption::decrypt_data(s, key); 35 | } 36 | Value::Object(map) => { 37 | // Check if this is a special MongoDB type, or if the key is "uid" 38 | let special_keys = [ 39 | "$oid", "$date", "$numberLong", "$binary", "$timestamp", "$regex", 40 | "$symbol", "$code", "$codeWithScope", "$minKey", "$maxKey", 41 | "$undefined", "$null", "$numberInt", "$numberDouble", "$numberDecimal" 42 | ]; 43 | 44 | for (k, v) in map.iter_mut() { 45 | if special_keys.contains(&k.as_str()) || k == "uid" { 46 | continue; 47 | } 48 | decrypt_value(v, key); 49 | } 50 | } 51 | _ => {} 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/traits/encryption.rs: -------------------------------------------------------------------------------- 1 | use serde::{de::DeserializeOwned, Serialize}; 2 | use serde_json::Value; 3 | 4 | use crate::utils::encryption_utils::Encryption; 5 | 6 | pub trait Encrypt { 7 | fn encrypt(&self, key: &str) -> Self; 8 | } 9 | 10 | impl Encrypt for T 11 | where 12 | T: Serialize + DeserializeOwned, 13 | { 14 | fn encrypt(&self, key: &str) -> Self { 15 | // Serialize the object to JSON 16 | let json_str = serde_json::to_string(self).unwrap(); 17 | 18 | // Deserialize the JSON string into a serde_json::Value 19 | let mut value: Value = serde_json::from_str(&json_str).unwrap(); 20 | 21 | // Encrypt the keys and values recursively 22 | encrypt_value(&mut value, key); 23 | 24 | // Deserialize the serde_json::Value back to the original object 25 | serde_json::from_value(value).unwrap() 26 | } 27 | } 28 | 29 | // Recursive function to encrypt object keys and values 30 | fn encrypt_value(value: &mut Value, key: &str) { 31 | match value { 32 | Value::String(s) => { 33 | // Encrypt string values 34 | *s = Encryption::encrypt_data(s, key); 35 | } 36 | Value::Object(map) => { 37 | // Check if this is a special MongoDB type, or if the key is "uid" 38 | let special_keys = [ 39 | "$oid", "$date", "$numberLong", "$binary", "$timestamp", "$regex", 40 | "$symbol", "$code", "$codeWithScope", "$minKey", "$maxKey", 41 | "$undefined", "$null", "$numberInt", "$numberDouble", "$numberDecimal" 42 | ]; 43 | 44 | for (k, v) in map.iter_mut() { 45 | if special_keys.contains(&k.as_str()) || k == "uid" { 46 | continue; 47 | } 48 | encrypt_value(v, key); 49 | } 50 | } 51 | _ => {} 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /k8s/local/mongodb-express-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: mongodb-express 5 | namespace: flexauth 6 | labels: 7 | app: mongodb-express 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: mongodb-express 13 | template: 14 | metadata: 15 | labels: 16 | app: mongodb-express 17 | spec: 18 | containers: 19 | - name: mongodb-express 20 | image: mongo-express 21 | ports: 22 | - containerPort: 8081 23 | env: 24 | - name: ME_CONFIG_MONGODB_SERVER 25 | valueFrom: 26 | configMapKeyRef: 27 | name: flexauth-config 28 | key: database_url 29 | - name: ME_CONFIG_MONGODB_ADMINUSERNAME 30 | valueFrom: 31 | secretKeyRef: 32 | name: flexauth-secrets 33 | key: MONGO_INITDB_ROOT_USERNAME 34 | - name: ME_CONFIG_MONGODB_ADMINPASSWORD 35 | valueFrom: 36 | secretKeyRef: 37 | name: flexauth-secrets 38 | key: MONGO_INITDB_ROOT_PASSWORD 39 | - name: ME_CONFIG_BASICAUTH_USERNAME 40 | valueFrom: 41 | secretKeyRef: 42 | name: flexauth-secrets 43 | key: MONGO_INITDB_ROOT_USERNAME 44 | - name: ME_CONFIG_BASICAUTH_PASSWORD 45 | valueFrom: 46 | secretKeyRef: 47 | name: flexauth-secrets 48 | key: MONGO_INITDB_ROOT_PASSWORD 49 | --- 50 | apiVersion: v1 51 | kind: Service 52 | metadata: 53 | name: mongodb-express-service 54 | namespace: flexauth 55 | spec: 56 | selector: 57 | app: mongodb-express 58 | type: LoadBalancer 59 | ports: 60 | - protocol: TCP 61 | port: 8081 62 | targetPort: 8081 -------------------------------------------------------------------------------- /src/utils/email_utils.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use lettre::message::header::ContentType; 4 | use lettre::transport::smtp::authentication::Credentials; 5 | use lettre::{Message, SmtpTransport, Transport}; 6 | 7 | pub struct Email { 8 | pub name: String, 9 | pub email: String, 10 | pub subject: String, 11 | pub body: String, 12 | } 13 | 14 | impl Email { 15 | pub fn new(name: &str, email: &str, subject: &str, body:&str) -> Self { 16 | Email { 17 | name: name.to_string(), 18 | email: email.to_string(), 19 | subject: subject.to_string(), 20 | body: body.to_string(), 21 | } 22 | } 23 | 24 | pub async fn send(&self) { 25 | let smtp_username = env::var("EMAIL").expect("EMAIL_ID must be set"); 26 | let smtp_password = env::var("EMAIL_PASSWORD").expect("EMAIL_PASSWORD must be set"); 27 | let name = env::var("MAIL_NAME").expect("NAME must be set"); 28 | let smtp_domain = env::var("SMTP_DOMAIN").expect("SMTP_DOMAIN must be set"); 29 | 30 | let from = format!("{} <{}>", name, smtp_username); 31 | let to = format!("{} <{}>", self.name, self.email); 32 | let message = Message::builder() 33 | .from(from.parse().unwrap()) 34 | .to(to.parse().unwrap()) 35 | .subject(self.subject.to_owned()) // Convert &str to String 36 | .header(ContentType::TEXT_PLAIN) 37 | .body(String::from(self.body.to_owned())) 38 | .unwrap(); 39 | 40 | let credentials = Credentials::new(smtp_username.to_owned(), smtp_password.to_owned()); 41 | 42 | // Open a remote connection to gmail 43 | let mailer = SmtpTransport::relay(&smtp_domain) 44 | .unwrap() 45 | .credentials(credentials) 46 | .build(); 47 | 48 | // Send the email 49 | match mailer.send(&message) { 50 | Ok(_) => println!("Email sent successfully!"), 51 | Err(e) => panic!("Could not send email: {e:?}"), 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/handlers/overview_handler.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::State, Json}; 2 | use axum_macros::debug_handler; 3 | use bson::doc; 4 | use bson::DateTime; 5 | 6 | use crate::core::session::Session; 7 | use crate::errors::Result; 8 | use crate::models::overview_model::OverviewResponse; 9 | use crate::{core::user::User, AppState}; 10 | 11 | #[debug_handler] 12 | pub async fn get_all_overview_handler( 13 | State(state): State, 14 | ) -> Result> { 15 | println!(">> HANDLER: get_all_overview_handler called"); 16 | 17 | let users = User::get_all(&state.mongo_client).await.unwrap(); 18 | let user_count = users.len(); 19 | let active_user_count = users.iter().filter(|u| u.is_active).count(); 20 | let inactive_user_count = users.iter().filter(|u| !u.is_active).count(); 21 | let blocked_user_count = users 22 | .iter() 23 | .filter(|u| u.blocked_until.map_or(false, |time| time > DateTime::now())) 24 | .count(); 25 | 26 | let all_sessions = Session::get_all(&state.mongo_client).await.unwrap(); 27 | println!(">> all_sessions Length: {:?}", all_sessions.len()); 28 | 29 | let active_session_count = all_sessions.iter().filter(|s| !s.is_revoked).count(); 30 | let revoked_session_count = all_sessions.iter().filter(|s| s.is_revoked).count(); 31 | 32 | let os_types: Vec = all_sessions 33 | .iter() 34 | .map(|session| session.os.clone()) 35 | .collect(); 36 | 37 | let device_types: Vec = all_sessions 38 | .iter() 39 | .map(|session| session.device.clone()) 40 | .collect(); 41 | 42 | let browser_types: Vec = all_sessions 43 | .iter() 44 | .map(|session| session.browser.clone()) 45 | .collect(); 46 | 47 | println!(">> os_types: {:?}", os_types); 48 | println!(">> device_types: {:?}", device_types); 49 | println!(">> browser_types: {:?}", browser_types); 50 | 51 | let response = OverviewResponse { 52 | user_count, 53 | active_user_count, 54 | inactive_user_count, 55 | blocked_user_count, 56 | revoked_session_count, 57 | active_session_count, 58 | os_types, 59 | device_types, 60 | browser_types, 61 | }; 62 | 63 | Ok(Json(response)) 64 | } 65 | -------------------------------------------------------------------------------- /src/models/session_model.rs: -------------------------------------------------------------------------------- 1 | use bson::DateTime; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Deserialize, Debug, Clone, Serialize)] 5 | pub struct VerifySession { 6 | pub token: String, 7 | } 8 | 9 | #[derive(Deserialize, Debug, Clone, Serialize)] 10 | pub struct SessionResponse { 11 | pub uid: String, 12 | pub session_id: String, 13 | pub email: String, 14 | pub user_agent: String, 15 | pub os: String, 16 | pub os_version: String, 17 | pub vendor: String, 18 | pub device: String, 19 | pub browser: String, 20 | pub browser_version: String, 21 | pub is_revoked: bool, 22 | pub created_at: DateTime, 23 | pub updated_at: DateTime, 24 | } 25 | 26 | #[derive(Deserialize, Debug, Clone)] 27 | pub struct SessionRefreshPayload { 28 | pub uid: String, 29 | pub session_id: String, 30 | pub id_token: String, 31 | pub refresh_token: String, 32 | } 33 | 34 | #[derive(Serialize, Debug, Clone)] 35 | pub struct SessionRefreshResult { 36 | pub uid: String, 37 | pub session_id: String, 38 | pub id_token: String, 39 | pub refresh_token: String, 40 | } 41 | 42 | #[derive(Deserialize, Debug, Clone)] 43 | pub struct RevokeAllSessionsPayload { 44 | pub uid: String, 45 | } 46 | 47 | #[derive(Serialize, Debug, Clone)] 48 | pub struct RevokeAllSessionsResult { 49 | pub message: String, 50 | } 51 | 52 | #[derive(Deserialize, Debug, Clone)] 53 | pub struct RevokeSessionsPayload { 54 | pub session_id: String, 55 | pub uid: String, 56 | } 57 | 58 | #[derive(Serialize, Debug, Clone)] 59 | pub struct RevokeSessionsResult { 60 | pub message: String, 61 | } 62 | 63 | #[derive(Deserialize, Debug, Clone)] 64 | pub struct DeleteAllSessionsPayload { 65 | pub uid: String, 66 | } 67 | 68 | #[derive(Serialize, Debug, Clone)] 69 | pub struct DeleteAllSessionsResult { 70 | pub message: String, 71 | } 72 | 73 | #[derive(Deserialize, Debug, Clone)] 74 | pub struct DeleteSessionsPayload { 75 | pub session_id: String, 76 | pub uid: String, 77 | } 78 | 79 | #[derive(Serialize, Debug, Clone)] 80 | pub struct DeleteSessionsResult { 81 | pub message: String, 82 | } 83 | 84 | #[derive(Deserialize, Debug, Clone)] 85 | pub struct SessionDetailsPayload { 86 | pub uid: String, 87 | pub session_id: String, 88 | } 89 | -------------------------------------------------------------------------------- /docs/backend/password-protection.md: -------------------------------------------------------------------------------- 1 | 2 | # Password protection 3 | 4 | Password protection is another core functionality for an auth server and we have taken that seriously. Here is a brief on how the password gets salted, hashed, encrypted, and stored in the database. 5 | 6 | 7 | 8 | ## Method 9 | 10 | They are using multiple hashing algorithms for **protecting passwords**. 11 | 12 | ### Hashing Algorithms used. 13 | - `Agron`: Hasing and salting - [carte link](https://crates.io/crates/argon2) 14 | - `Sha256` : Final Hashing - [carte link](https://crates.io/crates/sha256) 15 | 16 | 17 | ## Diagram 18 | 19 | ![password-protection-inhouse-auth](https://github.com/Rajdip019/in-house-auth/assets/91758830/bdee629b-ea6f-4a61-b3a2-989e0fcf2a11) 20 | 21 | 22 | ## Explaination 23 | 24 | Here is a step-by-step guide on how it works. 25 | 26 | ### Step 1: 27 | Raw Password is salted using a random salt created by the `Argon` library. And then hashed which looks like this - 28 | 29 | `$argon2i$v=19$m=16,t=2,p=1$S0FVN2NRbHF2RzBzOXBLSg$xLmzkDhV/z9qRPLpD2ybqw` 30 | 31 | You can generate and play with argon hashing library configurations here - [Argon2 online](https://argon2.online/) 32 | 33 | ### Step 2: 34 | The Agron hash is then put into `sha256` again to generate the more random and more random hash. It gives a 256bit hex which looks like this - `16567bc6bf75f0ac224749b27b42487012246768dbb8bce95b8638b6ab826ca01` 35 | 36 | ### Step 3: 37 | We encrypt the password using the user `DEK` using the `AESGcm256` algorithm and store it in DB this ensures a higher level of secure and unique hex string. 38 | 39 | ### Step 3: 40 | The auth server has its own `KEK`. This is unique for the server. You can generate it by running the command below from the root of your project. ( Make sure you have cargo installed ) - [How to install cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) 41 | ``` 42 | 43 | cargo run --bin create_kek 44 | 45 | ``` 46 | 47 | ### Step 4: 48 | We use the `KEK` to encrypt the `DEK` using the same `AESGcm256` algorithm and store it in DB. 49 | 50 | ### Step 5: ( Additional ) 51 | For additional safety, you can use `GCP KMS`, `AWS KMS` or any other cloud provider for additional safety. 52 | 53 | 54 | 55 | ## Feedback 56 | 57 | If you have any feedback, please raise an issue or start a discussion. Thank you. 58 | 59 | -------------------------------------------------------------------------------- /src/config/init.rs: -------------------------------------------------------------------------------- 1 | use mongodb::{Client, Collection}; 2 | 3 | use crate::core::{dek::Dek, user::User}; 4 | 5 | struct InitUser { 6 | name: String, 7 | email: String, 8 | role: String, 9 | password: String, 10 | } 11 | 12 | pub async fn init_users(mongo_client: Client) { 13 | // create a few users 14 | let users = vec![ 15 | InitUser { 16 | name: "Debajyoti Saha".to_string(), 17 | email: "debajyotisaha14@gmail.com".to_string(), 18 | role: "admin".to_string(), 19 | password: "Debu014@".to_string(), 20 | }, 21 | InitUser { 22 | name: "Rajdeep Sengupta".to_string(), 23 | email: "rajdipgupta019@gmail.com".to_string(), 24 | role: "admin".to_string(), 25 | password: "Rajdeep19@".to_string(), 26 | }, 27 | InitUser { 28 | name: "Sourav Banik".to_string(), 29 | email: "pachu@email.com".to_string(), 30 | role: "user".to_string(), 31 | password: "pachu20@".to_string(), 32 | }, 33 | ]; 34 | 35 | // check if the users already exist 36 | let db = mongo_client.database("auth"); 37 | let collection: Collection = db.collection("users"); 38 | let cursor = collection.count_documents(None, None).await.unwrap(); 39 | 40 | if cursor > 0 { 41 | println!(">> Users already exist. Skipping user creation."); 42 | return; 43 | } 44 | 45 | // map the users 46 | for user in users { 47 | // create a new user 48 | let new_user = User::new(&user.name, &user.email, &user.role, &user.password); 49 | let dek = Dek::generate(); 50 | match new_user.encrypt_and_add(&mongo_client, &dek).await { 51 | Ok(_) => {} 52 | Err(e) => { 53 | println!(">> Error adding user: {:?}", e); 54 | continue; 55 | } 56 | }; 57 | 58 | // add the dek to the deks collection 59 | match Dek::new(&new_user.uid, &new_user.email, &dek) 60 | .encrypt_and_add(&mongo_client) 61 | .await 62 | { 63 | Ok(_) => {} 64 | Err(e) => { 65 | println!(">> Error adding dek: {:?}", e); 66 | continue; 67 | } 68 | } 69 | 70 | println!(">> {:?} added. uid: {:?}", new_user.name, new_user.uid); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/middlewares/with_api_key.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | body::Body, 3 | http::{Request, Response, StatusCode}, 4 | middleware::Next, 5 | response::IntoResponse, 6 | Json, 7 | }; 8 | use serde::Serialize; 9 | use std::env; 10 | use std::time::{SystemTime, UNIX_EPOCH}; 11 | use uuid::Uuid; 12 | 13 | #[derive(Serialize)] 14 | struct ErrorResponse { 15 | error: ErrorResponseDetails, 16 | } 17 | 18 | #[derive(Serialize)] 19 | struct ErrorResponseDetails { 20 | status: u16, 21 | message: String, 22 | req_uuid: String, 23 | } 24 | 25 | async fn log_request(uuid: &str, req_method: &str, uri: &str, status: u16, message: &str) { 26 | let timestamp = SystemTime::now() 27 | .duration_since(UNIX_EPOCH) 28 | .unwrap() 29 | .as_millis() 30 | .to_string(); 31 | 32 | println!( 33 | "RequestLog: {{ uuid: {}, timestamp: {}, req_method: {}, uri: {}, status: {}, message: {} }}", 34 | uuid, timestamp, req_method, uri, status, message 35 | ); 36 | } 37 | 38 | pub async fn with_api_key(req: Request, next: Next) -> Result, StatusCode> { 39 | let expected_api_key = env::var("X_API_KEY").unwrap_or_default(); 40 | let api_key = req.headers().get("x-api-key").and_then(|key| key.to_str().ok()); 41 | 42 | let uuid = Uuid::new_v4().to_string(); 43 | let req_method = req.method().to_string(); 44 | let req_uri = req.uri().to_string(); 45 | 46 | if expected_api_key.is_empty() { 47 | let status = StatusCode::INTERNAL_SERVER_ERROR; 48 | let message = "Internal Server Error: API key is not configured".to_string(); 49 | 50 | log_request(&uuid, &req_method, &req_uri, status.as_u16(), &message).await; 51 | 52 | let error_response = ErrorResponse { 53 | error: ErrorResponseDetails { 54 | status: status.as_u16(), 55 | message, 56 | req_uuid: uuid.clone(), 57 | }, 58 | }; 59 | 60 | return Ok((status, Json(error_response)).into_response()); 61 | } 62 | 63 | if let Some(key) = api_key { 64 | if key == expected_api_key { 65 | return Ok(next.run(req).await); 66 | } 67 | } 68 | 69 | let status = StatusCode::UNAUTHORIZED; 70 | let message = "Unauthorized: Invalid API key".to_string(); 71 | 72 | log_request(&uuid, &req_method, &req_uri, status.as_u16(), &message).await; 73 | 74 | let error_response = ErrorResponse { 75 | error: ErrorResponseDetails { 76 | status: status.as_u16(), 77 | message, 78 | req_uuid: uuid.clone(), 79 | }, 80 | }; 81 | 82 | Ok((status, Json(error_response)).into_response()) 83 | } 84 | -------------------------------------------------------------------------------- /docs/backend/session-managment.md: -------------------------------------------------------------------------------- 1 | 2 | # Session Management 3 | 4 | The session management is done in such a way that you can track each and every user session while giving users long-lived sessions on multiple devices. Also, any detected miscellaneous activity leads to revoking the user session. 5 | 6 | 7 | ## Tokens issues for a session 8 | 9 | We are using multiple tokens for a **session**. 10 | 11 | ### Hashing Algorithms used. 12 | - `Session ID`: Each session of a new device/browser makes a new user session and is encrypted by the user `DEK`. For more info. Session ID id is the main session Identifier - [User Data Protection](https://github.com/Rajdip019/in-house-auth/blob/main/docs/backend/user-data-protection.md) 13 | - `ID Token`: Holds the identity of the user. An ID token is lived for 1 hour. 14 | - `Refresh Token`: Holds the capability to refresh the session. A refresh token lives for 45 days. Although the refresh token life is shorted by the ID Token as on refresh token can refresh only one session it is paired with. 15 | 16 | 17 | ## Verify Session 18 | 19 | While verifying a session from the `session/verify` route. We check if the ID Token is valid and not expired. If both are satisfied it returns user data which you can store in a user state. 20 | 21 | ## Refresh Session 22 | 23 | In the refresh session first, we ensure that the `ID Token` is already expired, the `Refresh Token` is not expired, and the `ID Token`, `Refresh Token`, and `Session ID` is paired in the same session. 24 | 25 | Next, we validate if the user agent for the session is the same or not. If not then we revoke the session and mail the user for malicious activity. 26 | 27 | If all goes good then we issue a new pair of `ID Token` and `Refresh Token` and send it back to the user. 28 | 29 | 30 | ## Brute Force protection for Password 31 | 32 | We maintain a consecutive failed attempted sign-in count and block the user for some time based on that and also send an email to the user about that and give the device info as well as which device is trying to do this. 33 | 34 | - 5 consecutive wrong passwords - 180 seconds block 35 | - 10 consecutive wrong passwords - 600 seconds block 36 | - 15 consecutive wrong passwords - 3600 seconds block 37 | 38 | For now, there is no rate limiting by the server itself we highly recommend you do that by using an external service. We will soon implement that natively as well. 39 | 40 | ## More malicious activity protection 41 | - If a refresh session is asked and the `ID Token`, `Refresh Token`, and `Session ID` are not paired together we revoke the token immediately. Like if a wrong Refresh token or Session ID is passed for a session ID the session gets blocked. 42 | 43 | - We also have a revoke-all-session endpoint for users that can be used to sign out from all devices/browsers of the user. 44 | 45 | ## Feedback 46 | 47 | If you have any feedback, please raise an issue or start a discussion. Thank you. 48 | -------------------------------------------------------------------------------- /k8s/local/flexauth-service-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: flexauth-server 5 | namespace: flexauth 6 | labels: 7 | app: flexauth-server 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: flexauth-server 13 | template: 14 | metadata: 15 | labels: 16 | app: flexauth-server 17 | spec: 18 | containers: 19 | - name: flexauth-server 20 | image: flexauth-server:latest 21 | imagePullPolicy: IfNotPresent 22 | ports: 23 | - containerPort: 8080 24 | env: 25 | - name: MONGO_URI 26 | valueFrom: 27 | configMapKeyRef: 28 | name: flexauth-config 29 | key: database_url 30 | - name: PORT 31 | valueFrom: 32 | secretKeyRef: 33 | name: flexauth-secrets 34 | key: PORT 35 | - name: SERVER_KEK 36 | valueFrom: 37 | secretKeyRef: 38 | name: flexauth-secrets 39 | key: SERVER_KEK 40 | - name: EMAIL 41 | valueFrom: 42 | secretKeyRef: 43 | name: flexauth-secrets 44 | key: EMAIL 45 | - name: EMAIL_PASSWORD 46 | valueFrom: 47 | secretKeyRef: 48 | name: flexauth-secrets 49 | key: EMAIL_PASSWORD 50 | - name: MAIL_NAME 51 | valueFrom: 52 | secretKeyRef: 53 | name: flexauth-secrets 54 | key: MAIL_NAME 55 | - name: SMTP_DOMAIN 56 | valueFrom: 57 | secretKeyRef: 58 | name: flexauth-secrets 59 | key: SMTP_DOMAIN 60 | - name: SMTP_PORT 61 | valueFrom: 62 | secretKeyRef: 63 | name: flexauth-secrets 64 | key: SMTP_PORT 65 | - name: X_API_KEY 66 | valueFrom: 67 | secretKeyRef: 68 | name: flexauth-secrets 69 | key: X_API_KEY 70 | - name: MONGO_INITDB_ROOT_PASSWORD 71 | valueFrom: 72 | secretKeyRef: 73 | name: flexauth-secrets 74 | key: MONGO_INITDB_ROOT_PASSWORD 75 | - name: MONGO_INITDB_ROOT_USERNAME 76 | valueFrom: 77 | secretKeyRef: 78 | name: flexauth-secrets 79 | key: MONGO_INITDB_ROOT_USERNAME 80 | 81 | --- 82 | apiVersion: v1 83 | kind: Service 84 | metadata: 85 | name: flexauth-service 86 | namespace: flexauth 87 | spec: 88 | type: LoadBalancer 89 | selector: 90 | app: flexauth-server 91 | ports: 92 | - protocol: TCP 93 | port: 8080 94 | targetPort: 8080 95 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG RUST_VERSION=1.77 4 | ARG APP_NAME=inhouse-auth 5 | 6 | ################################################################################ 7 | # Create a development stage for building the application. 8 | FROM rust:${RUST_VERSION}-alpine AS dev 9 | 10 | # Set the working directory 11 | WORKDIR /app 12 | 13 | # Install system dependencies and required libraries for the build 14 | RUN apk add --no-cache musl-dev pkgconfig openssl-dev openssl-libs-static 15 | 16 | # Install cargo-watch for auto-reloading 17 | RUN cargo install cargo-watch --locked 18 | 19 | # Copy the Cargo.toml and Cargo.lock files separately to cache dependencies 20 | COPY Cargo.toml Cargo.lock ./ 21 | 22 | # Create a dummy source file and build dependencies to cache them 23 | RUN mkdir src && echo "fn main() {}" > src/main.rs 24 | RUN cargo build --release || true 25 | RUN rm -rf src 26 | 27 | # Copy the actual source code 28 | COPY . . 29 | 30 | # Mount the source code into the container 31 | VOLUME /app/src 32 | 33 | # Entrypoint command 34 | CMD ["cargo", "watch", "-q", "-c" ,"-x", "run"] 35 | 36 | ################################################################################ 37 | # Create a stage for building the application. 38 | 39 | FROM rust:${RUST_VERSION}-alpine AS build 40 | ARG APP_NAME 41 | WORKDIR /app 42 | 43 | # Install host build dependencies. 44 | RUN apk add --no-cache clang lld musl-dev git 45 | 46 | # Install OpenSSL development libraries 47 | RUN apk add --no-cache pkgconfig openssl-dev 48 | 49 | # -lssl -lcrypto are required for the openssl crate 50 | RUN apk add --no-cache openssl-libs-static 51 | 52 | 53 | RUN cargo install cargo-watch --locked 54 | 55 | 56 | RUN --mount=type=bind,source=src,target=src \ 57 | --mount=type=bind,source=Cargo.toml,target=Cargo.toml \ 58 | --mount=type=bind,source=Cargo.lock,target=Cargo.lock \ 59 | --mount=type=cache,target=/app/target/ \ 60 | --mount=type=cache,target=/usr/local/cargo/git/db \ 61 | --mount=type=cache,target=/usr/local/cargo/registry/ \ 62 | cargo build --locked --release && \ 63 | cp ./target/release/$APP_NAME /bin/server 64 | 65 | ################################################################################` 66 | 67 | ## Final build file 68 | 69 | FROM alpine:3.18 AS final 70 | 71 | ARG PORT 72 | 73 | # Create a non-privileged user that the app will run under. 74 | # See https://docs.docker.com/go/dockerfile-user-best-practices/ 75 | ARG UID=10001 76 | RUN adduser \ 77 | --disabled-password \ 78 | --gecos "" \ 79 | --home "/nonexistent" \ 80 | --shell "/sbin/nologin" \ 81 | --no-create-home \ 82 | --uid "${UID}" \ 83 | appuser 84 | USER appuser 85 | 86 | # Copy the executable from the "build" stage. 87 | COPY --from=build /bin/server /bin/ 88 | 89 | # Expose the port that the application listens on. 90 | EXPOSE ${PORT} 91 | 92 | # What the container should run when it is started. 93 | CMD ["/bin/server"] 94 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use axum::extract::State; 2 | use axum::response::Html; 3 | use axum::routing::get; 4 | use axum::{middleware, Router}; 5 | use dotenv::dotenv; 6 | use handlers::password_handler::forget_password_form; 7 | use handlers::user_handler::{show_block_user_page, show_verification_page_email}; 8 | use middlewares::res_log::main_response_mapper; 9 | use middlewares::with_api_key::with_api_key; 10 | use mongodb::Client; 11 | use std::error::Error; 12 | 13 | mod config; 14 | mod core; 15 | mod errors; 16 | mod handlers; 17 | mod middlewares; 18 | mod models; 19 | mod routes; 20 | mod traits; 21 | mod utils; 22 | 23 | #[derive(Clone)] 24 | struct AppState { 25 | mongo_client: Client, 26 | } 27 | 28 | #[tokio::main] 29 | async fn main() -> Result<(), Box> { 30 | dotenv().ok(); 31 | let mongo_client = config::db_connection_handler::connect().await?; 32 | // init users if not exists 33 | config::init::init_users(mongo_client.clone()).await; 34 | 35 | let app_state = AppState { mongo_client }; 36 | // Define routes where middleware is applied 37 | let protected_routes = Router::new() 38 | .merge(routes::auth_routes::routes(State(app_state.clone()))) 39 | .merge(routes::user_routes::routes(State(app_state.clone()))) 40 | .merge(routes::password_routes::routes(State(app_state.clone()))) 41 | .merge(routes::session_routes::routes(State(app_state.clone()))) 42 | .merge(routes::overview_routes::routes(State(app_state.clone()))) 43 | .layer(middleware::map_response(main_response_mapper)) 44 | .layer(middleware::from_fn(with_api_key)); 45 | 46 | // Define routes where middleware is not applied 47 | let public_routes = Router::new() 48 | .route("/", get(root_handler)) 49 | .route("/forget-reset/:id", get(forget_password_form)) 50 | .route("/verify-email/:id", get(show_verification_page_email)) 51 | .route("/block-account/:id", get(show_block_user_page)) 52 | .merge(routes::health_check_routes::routes()) 53 | .layer(middleware::map_response(main_response_mapper)); 54 | 55 | // Combine public and protected routes 56 | let app = Router::new() 57 | .nest("/api", protected_routes) 58 | .nest("/", public_routes); 59 | 60 | let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap(); 61 | axum::serve::serve(listener, app.into_make_service()) 62 | .await 63 | .unwrap(); 64 | Ok(()) 65 | } 66 | 67 | async fn root_handler() -> Html<&'static str> { 68 | let html_content = r#" 69 | 70 | 71 | 72 | FlexAuth 73 | 74 | 75 |

Welcome to FlexAuth

76 |

Your own flexible, blazingly fast 🦀, and secure in-house authentication system.

77 |

Here's the API documentation.

78 | 79 | 80 | "#; 81 | Html(html_content) 82 | } 83 | -------------------------------------------------------------------------------- /src/middlewares/res_log.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | 3 | use crate::errors::{ClientError, Error}; 4 | use axum::{http::{Method, Uri}, response::IntoResponse, Json}; 5 | use serde::Serialize; 6 | use serde_json::{json, Value}; 7 | use serde_with::skip_serializing_none; 8 | use axum::response::Result; 9 | use bson::Uuid; 10 | use axum::response::Response; 11 | 12 | #[skip_serializing_none] 13 | #[derive(Serialize)] 14 | struct RequestLogLine { 15 | uuid: String, 16 | timestamp: String, 17 | 18 | // -- http request attributes 19 | req_path: String, 20 | req_method: String, 21 | 22 | // -- Errors attributes 23 | client_error_type: Option, 24 | error_type: Option, 25 | error_data: Option, 26 | } 27 | 28 | async fn log_request( 29 | uuid: String, 30 | req_method: Method, 31 | uri: Uri, 32 | service_error: Option, 33 | client_error: Option, 34 | ) -> Result<()> { 35 | let timestamp = SystemTime::now() 36 | .duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis(); 37 | 38 | let error_type = service_error.clone().map(|se| se.as_ref().to_string()); 39 | let error_data = serde_json::to_value(service_error) 40 | .ok() 41 | .and_then(|mut v| v.get_mut("data").map(|v| v.take())); 42 | 43 | // Create the RequestLogLine 44 | let log_line = RequestLogLine { 45 | uuid, 46 | timestamp: timestamp.to_string(), 47 | 48 | req_path: uri.path().to_string(), 49 | req_method: req_method.to_string(), 50 | 51 | client_error_type: client_error.map(|ce| ce.as_ref().to_string()), 52 | 53 | error_type, 54 | error_data, 55 | }; 56 | 57 | println!(">> Log Line: \n{:?}", json!(log_line)); 58 | 59 | Ok(()) 60 | } 61 | 62 | pub async fn main_response_mapper( 63 | uri: Uri, 64 | req_method: Method, 65 | res: Response 66 | ) -> Response { 67 | let uuid = Uuid::new(); 68 | 69 | // Get the eventual response error 70 | let service_error = res.extensions().get::(); 71 | let client_status_error = service_error.map(|e| e.client_status_and_error()); 72 | 73 | // -- If client error, build the new response 74 | let error_response = client_status_error.as_ref().map(|(status, client_error)| { 75 | let client_error_body = json!({"error": { 76 | "type": client_error.as_ref(), 77 | "req_uuid": uuid.to_string(), 78 | } 79 | }); 80 | 81 | println!(">> Client Error: {:?}", client_error_body); 82 | (*status, Json(client_error_body)).into_response() 83 | }); 84 | 85 | println!(">> Server Log line - {uuid} - Error: {error:?}", uuid = uuid, error = client_status_error); 86 | 87 | // Build and log the request log line 88 | let client_error = client_status_error.unzip().1; 89 | log_request( 90 | uuid.to_string(), 91 | req_method, 92 | uri, 93 | service_error.cloned(), 94 | client_error, 95 | ).await.unwrap(); 96 | 97 | println!(); 98 | error_response.unwrap_or(res) 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/models/user_model.rs: -------------------------------------------------------------------------------- 1 | use core::str; 2 | 3 | use bson::{oid::ObjectId, DateTime}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::core::user::User; 7 | 8 | #[derive(Serialize, Deserialize, Debug, Clone)] 9 | pub struct UserList { 10 | pub users: Vec, 11 | } 12 | 13 | #[derive(Deserialize, Debug, Clone)] 14 | pub struct UserEmailPayload { 15 | pub email: String, 16 | } 17 | 18 | #[derive(Serialize, Debug, Clone)] 19 | pub struct UserEmailResponse { 20 | pub message: String, 21 | pub email: String, 22 | } 23 | 24 | #[derive(Deserialize, Debug, Clone)] 25 | pub struct UserIdPayload { 26 | pub uid: String, 27 | } 28 | 29 | #[derive(Serialize, Deserialize, Debug, Clone)] 30 | pub struct UserId { 31 | pub uid: String, 32 | } 33 | 34 | #[derive(Deserialize, Debug, Clone)] 35 | pub struct UpdateUserPayload { 36 | pub name: String, 37 | pub email: String, 38 | } 39 | 40 | #[derive(Serialize, Deserialize, Debug, Clone)] 41 | pub struct UpdateUserResponse { 42 | pub email: String, 43 | pub name: String, 44 | } 45 | 46 | #[derive(Deserialize, Debug, Clone)] 47 | pub struct UpdateUserRolePayload { 48 | pub role: String, 49 | pub email: String, 50 | } 51 | #[derive(Serialize, Debug, Clone)] 52 | pub struct UpdateUserRoleResponse { 53 | pub message: String, 54 | pub email: String, 55 | pub role: String, 56 | } 57 | 58 | #[derive(Deserialize, Debug, Clone)] 59 | pub struct ToggleUserActivationStatusPayload { 60 | pub is_active: Option, 61 | pub email: String, 62 | } 63 | 64 | #[derive(Serialize, Debug, Clone)] 65 | pub struct ToggleUserActivationStatusResponse { 66 | pub message: String, 67 | pub email: String, 68 | pub is_active: bool, 69 | } 70 | 71 | #[derive(Serialize, Deserialize, Debug, Clone)] 72 | pub struct UserResponse { 73 | pub uid: String, 74 | pub name: String, 75 | pub role: String, 76 | pub email: String, 77 | pub email_verified: bool, 78 | pub is_active: bool, 79 | pub blocked_until: Option, 80 | pub created_at: Option, 81 | pub updated_at: Option, 82 | } 83 | 84 | #[derive(Deserialize, Debug, Clone)] 85 | pub struct RecentUserPayload { 86 | pub limit: i64, 87 | } 88 | 89 | #[derive(Serialize, Deserialize, Debug, Clone)] 90 | pub struct EmailVerificationRequest { 91 | pub _id: ObjectId, 92 | pub req_id: String, 93 | pub uid: String, 94 | pub email: String, 95 | pub expires_at: DateTime, 96 | pub created_at: Option, 97 | pub updated_at: Option, 98 | } 99 | 100 | #[derive(Serialize, Deserialize, Debug, Clone)] 101 | pub struct EmailVerificationPayload { 102 | pub req_id: String, 103 | } 104 | #[derive(Serialize, Debug, Clone)] 105 | pub struct EmailVerificationResponse { 106 | pub message: String, 107 | pub req_id: String, 108 | } 109 | 110 | #[derive(Serialize, Deserialize, Debug, Clone)] 111 | pub struct UserBlockRequest { 112 | pub _id: ObjectId, 113 | pub req_id: String, 114 | pub uid: String, 115 | pub email: String, 116 | pub is_used: bool, 117 | pub expires_at: DateTime, 118 | pub created_at: Option, 119 | pub updated_at: Option, 120 | } 121 | 122 | #[derive(Serialize, Deserialize, Debug, Clone)] 123 | pub struct BlockUserPayload { 124 | pub req_id: String, 125 | } 126 | 127 | #[derive(Serialize, Debug, Clone)] 128 | pub struct BlockUserResponse { 129 | pub message: String, 130 | pub req_id: String, 131 | } 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Flexauth

2 |

Your own flexible, blazingly fast 🦀, and secure in-house authentication system.

3 | 4 |
5 |

6 | 7 | [![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com) 8 | ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge) 9 | ![GitHub forks](https://img.shields.io/github/forks/Rajdip019/flexauth?style=for-the-badge) 10 | ![GitHub Repo stars](https://img.shields.io/github/stars/Rajdip019/flexauth?style=for-the-badge) 11 | ![GitHub contributors](https://img.shields.io/github/contributors/Rajdip019/flexauth?style=for-the-badge) 12 | ![GitHub last commit](https://img.shields.io/github/last-commit/Rajdip019/flexauth?style=for-the-badge) 13 | ![GitHub repo size](https://img.shields.io/github/repo-size/Rajdip019/flexauth?style=for-the-badge) 14 | ![Github](https://img.shields.io/github/license/Rajdip019/flexauth?style=for-the-badge) 15 | ![GitHub issues](https://img.shields.io/github/issues/Rajdip019/flexauth?style=for-the-badge) 16 | ![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/Rajdip019/flexauth?style=for-the-badge) 17 | ![GitHub pull requests](https://img.shields.io/github/issues-pr/Rajdip019/flexauth?style=for-the-badge) 18 | ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/Rajdip019/flexauth?style=for-the-badge) 19 | 20 |

21 |
22 | 23 | ## Introduction 24 | 25 | ![Flexauth Twitter Post](https://github.com/Rajdip019/flexauth/assets/91758830/1f4c9b68-b931-4c89-a17e-03bbc5983dd3) 26 | Flexauth is a flexible, blazingly fast 🦀, and secure auth system that you can use for your project/company* . Need to add a specific feature as you need? Change the code as you like and deploy it on your servers. Here is the most flexible auth system for all your needs. 27 | 28 | Need some features you think might be helpful for others? Raise a PR and we will surely try to bring it live. 29 | 30 | ## Local Setup: 31 | Here is a documentation for if you want to setup this repo locally - [Local Setup Doc](https://github.com/Rajdip019/flexauth/tree/main/docs/local-setup/readme.md) 32 | 33 | ## API Documentation: 34 | Here is the API documentation - [API Doc](https://documenter.getpostman.com/view/18827552/2sA3JT4Jmd#d47f6b6b-3e50-49f1-90e1-c206f8e08957) 35 | 36 | ## Backend Documentation: 37 | Link to backend docs - [Backend Doc](https://github.com/Rajdip019/flexauth/tree/main/docs/backend) 38 | 39 | ## Folder Structure and others: 40 | Link to general docs for this repository - [General Docs](https://github.com/Rajdip019/flexauth/tree/main/docs/folder-structure/readme.md) 41 | 42 | **Code of Conduct:** 43 | 44 | Please note that we have a code of conduct in place to ensure a welcoming and inclusive community for all contributors. Be respectful and considerate towards others, regardless of background or experience. Harassment or abusive behavior will not be tolerated. 45 | 46 | **Get Started:** 47 | 48 | Ready to start contributing to FlexAuth? Head over to our GitHub repository and fork the project today! 49 | 50 | 51 | [GitHub Repository Link](https://github.com/Rajdip019/flexAuth) 52 | 53 | We look forward to your contributions and thank you for helping us make FlexAuth the best platform for designers and teams! 54 | 55 | 56 |

Project maintainers

57 | 58 | 59 | 66 | 73 | 74 |
60 | 61 | Debajyoti Saha 62 |
63 | Rajdeep Sengupta 64 |
65 |
67 | 68 | Debajyoti Saha 69 |
70 | Debajyoti Saha 71 |
72 |
75 | 76 | 77 | -------------------------------------------------------------------------------- /src/handlers/auth_handler.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::State, 3 | http::{header, HeaderMap}, 4 | Json, 5 | }; 6 | use axum_macros::debug_handler; 7 | 8 | use crate::{ 9 | core::{auth::Auth, session::Session, user::User}, 10 | errors::{Error, Result}, 11 | models::{ 12 | auth_model::{SignInOrSignUpResponse, SignInPayload, SignUpPayload}, 13 | session_model::{RevokeSessionsPayload, RevokeSessionsResult}, 14 | }, 15 | utils::validation_utils::Validation, 16 | AppState, 17 | }; 18 | 19 | #[debug_handler] 20 | pub async fn signup_handler( 21 | State(state): State, 22 | header: HeaderMap, 23 | payload: Json, 24 | ) -> Result> { 25 | println!(">> HANDLER: signup_handler called"); 26 | 27 | // check if the payload is empty 28 | if payload.name.is_empty() 29 | || payload.email.is_empty() 30 | || payload.role.is_empty() 31 | || payload.password.is_empty() 32 | { 33 | return Err(Error::InvalidPayload { 34 | message: "Invalid payload".to_string(), 35 | }); 36 | } 37 | 38 | if !Validation::email(&payload.email) { 39 | return Err(Error::InvalidEmail { 40 | message: "Invalid Email".to_string(), 41 | }); 42 | } 43 | 44 | let user = User::get_from_email(&state.mongo_client, &payload.email).await; 45 | if user.is_ok() { 46 | return Err(Error::UserAlreadyExists { 47 | message: "User already exists".to_string(), 48 | }); 49 | } 50 | 51 | if !Validation::password(&payload.password) { 52 | return Err(Error::InvalidPassword { 53 | message: "The password must contain at least one alphabetic character (uppercase or lowercase), at least one digit, and must be at least 8 characters long.".to_string(), 54 | }); 55 | } 56 | 57 | // get user-agent form the header 58 | let user_agent = match header.get(header::USER_AGENT) { 59 | Some(ua) => ua.to_str().unwrap().to_string(), 60 | None => "".to_string(), 61 | }; 62 | 63 | if user_agent.is_empty() { 64 | return Err(Error::InvalidUserAgent { 65 | message: "Invalid User Agent, Can't let random user to signin".to_string(), 66 | }); 67 | } 68 | 69 | match Auth::sign_up( 70 | &state.mongo_client, 71 | &payload.name, 72 | &payload.email, 73 | &payload.role, 74 | &payload.password, 75 | &user_agent, 76 | ) 77 | .await 78 | { 79 | Ok(res) => Ok(Json(res)), 80 | Err(e) => Err(e), 81 | } 82 | } 83 | 84 | pub async fn signin_handler( 85 | State(state): State, 86 | header: HeaderMap, 87 | payload: Json, 88 | ) -> Result> { 89 | println!(">> HANDLER: signin_handler called"); 90 | // check if the payload is empty 91 | if payload.email.is_empty() || payload.password.is_empty() { 92 | return Err(Error::InvalidPayload { 93 | message: "Invalid payload".to_string(), 94 | }); 95 | } 96 | 97 | if !Validation::email(&payload.email) { 98 | return Err(Error::InvalidEmail { 99 | message: "Invalid Email".to_string(), 100 | }); 101 | } 102 | 103 | // get user-agent form the header 104 | let user_agent = match header.get(header::USER_AGENT) { 105 | Some(ua) => ua.to_str().unwrap().to_string(), 106 | None => "".to_string(), 107 | }; 108 | 109 | if user_agent.is_empty() { 110 | return Err(Error::InvalidUserAgent { 111 | message: "Invalid User Agent, Can't let random user to signin".to_string(), 112 | }); 113 | } 114 | 115 | match Auth::sign_in( 116 | &state.mongo_client, 117 | &payload.email, 118 | &payload.password, 119 | &user_agent, 120 | ) 121 | .await 122 | { 123 | Ok(res) => Ok(Json(res)), 124 | Err(e) => Err(e), 125 | } 126 | } 127 | 128 | pub async fn signout_handler( 129 | State(state): State, 130 | payload: Json, 131 | ) -> Result> { 132 | println!(">> HANDLER: signout_handler called"); 133 | 134 | if payload.session_id.is_empty() | payload.uid.is_empty() { 135 | return Err(Error::InvalidPayload { 136 | message: "Invalid payload passed".to_string(), 137 | }); 138 | } 139 | 140 | match Session::revoke(&state.mongo_client, &payload.session_id, &payload.uid).await { 141 | Ok(_) => Ok(Json(RevokeSessionsResult { 142 | message: "Session revoked successfully".to_string(), 143 | })), 144 | Err(e) => Err(e), 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/core/dek.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use aes_gcm::{aead::OsRng, AeadCore, Aes256Gcm, KeyInit}; 4 | use bson::{doc, oid::ObjectId, DateTime}; 5 | use mongodb::{Client, Collection}; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::{ 9 | errors::{Error, Result}, 10 | traits::{decryption::Decrypt, encryption::Encrypt}, utils::encryption_utils::Encryption, 11 | }; 12 | 13 | #[derive(Deserialize, Debug, Clone, Serialize)] 14 | pub struct Dek { 15 | pub _id: ObjectId, 16 | pub uid: String, 17 | pub email: String, 18 | pub dek: String, 19 | pub created_at: DateTime, 20 | pub updated_at: DateTime, 21 | } 22 | 23 | impl Dek { 24 | pub fn new(uid: &str, email: &str, dek: &str) -> Self { 25 | Self { 26 | _id: ObjectId::new(), 27 | uid: uid.to_string(), 28 | email: email.to_string(), 29 | dek: dek.to_string(), 30 | created_at: DateTime::now(), 31 | updated_at: DateTime::now(), 32 | } 33 | } 34 | 35 | pub fn generate() -> String { 36 | let key = Aes256Gcm::generate_key(OsRng); 37 | // convert the key to hex string 38 | let hex_key = key 39 | .iter() 40 | .map(|b| format!("{:02x}", b)) 41 | .collect::() 42 | .chars() 43 | .take(32) 44 | .collect::(); 45 | let iv = Aes256Gcm::generate_nonce(&mut OsRng); 46 | // convert the iv to hex string 47 | let hex_iv = iv 48 | .iter() 49 | .map(|b| format!("{:02x}", b)) 50 | .collect::() 51 | .chars() 52 | .take(12) 53 | .collect::(); 54 | // connect the key and iv with . between them 55 | let key_iv = format!("{}.{}", hex_key, hex_iv); 56 | return key_iv; 57 | } 58 | 59 | pub async fn encrypt_and_add(&self, mongo_client: &Client) -> Result { 60 | let db = mongo_client.database("auth"); 61 | let collection_dek: Collection = db.collection("deks"); 62 | 63 | let server_kek = env::var("SERVER_KEK").expect("Server Kek must be set."); 64 | 65 | let encrypted_dek = self.encrypt(&server_kek); 66 | 67 | match collection_dek.insert_one(&encrypted_dek, None).await { 68 | Ok(_) => return Ok(self.clone()), 69 | Err(e) => return Err(Error::ServerError { 70 | message: e.to_string(), 71 | }), 72 | } 73 | } 74 | 75 | pub async fn get(mongo_client: &Client, identifier: &str) -> Result { 76 | let db = mongo_client.database("auth"); 77 | let collection_dek: Collection = db.collection("deks"); 78 | 79 | let server_kek = env::var("SERVER_KEK").expect("Server Kek must be set."); 80 | 81 | // check if the identifier is a email or uid using regex 82 | let email_regex = 83 | regex::Regex::new(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$").unwrap(); 84 | let is_email = email_regex.is_match(identifier); 85 | match is_email { 86 | true => { 87 | // encrypt the email using kek 88 | let encrypted_identifier = Encryption::encrypt_data(&identifier, &server_kek); 89 | let cursor_dek = collection_dek 90 | .find_one( 91 | Some(doc! { 92 | "email": encrypted_identifier.clone(), 93 | }), 94 | None, 95 | ) 96 | .await 97 | .unwrap(); 98 | 99 | match cursor_dek { 100 | Some(data) => return Ok(data.decrypt(&server_kek)), 101 | None => { 102 | return Err(Error::KeyNotFound { 103 | message: "DEK not found".to_string(), 104 | }); 105 | } 106 | }; 107 | } 108 | false => { 109 | let cursor_dek = collection_dek 110 | .find_one( 111 | Some(doc! { 112 | "uid": identifier, 113 | }), 114 | None, 115 | ) 116 | .await 117 | .unwrap(); 118 | 119 | match cursor_dek { 120 | Some(data) => return Ok(data.decrypt(&server_kek)), 121 | None => { 122 | return Err(Error::KeyNotFound { 123 | message: "DEK not found".to_string(), 124 | }); 125 | } 126 | }; 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/core/auth.rs: -------------------------------------------------------------------------------- 1 | use bson::{doc, DateTime}; 2 | use mongodb::{Client, Collection}; 3 | 4 | use crate::{ 5 | core::{dek::Dek, session::Session, user::User}, 6 | errors::{Error, Result}, 7 | models::auth_model::{SessionResponseForSignInOrSignUp, SignInOrSignUpResponse}, 8 | utils::{encryption_utils::Encryption, password_utils::Password}, 9 | }; 10 | 11 | pub struct Auth; 12 | 13 | impl Auth { 14 | pub async fn sign_up( 15 | mongo_client: &Client, 16 | name: &str, 17 | email: &str, 18 | role: &str, 19 | password: &str, 20 | user_agent: &str, 21 | ) -> Result { 22 | let db = mongo_client.database("auth"); 23 | 24 | let collection: Collection = db.collection("users"); 25 | let cursor = collection 26 | .find_one( 27 | Some(doc! { 28 | "email": email 29 | }), 30 | None, 31 | ) 32 | .await 33 | .unwrap(); 34 | 35 | if cursor.is_some() { 36 | return Err(Error::UserAlreadyExists { 37 | message: "User already exists".to_string(), 38 | }); 39 | } 40 | 41 | let dek = Dek::generate(); // create a data encryption key for new user 42 | let user = match User::new(name, email, role, password) 43 | .encrypt_and_add(&mongo_client, &dek) 44 | .await 45 | { 46 | Ok(user) => user, 47 | Err(e) => return Err(e), 48 | }; 49 | 50 | // add the dek to the deks collection 51 | let dek_data = match Dek::new(&user.uid, &user.email, &dek) 52 | .encrypt_and_add(&mongo_client) 53 | .await 54 | { 55 | Ok(dek_data) => dek_data, 56 | Err(e) => return Err(e), 57 | }; 58 | 59 | let session = match Session::new(&user, user_agent) 60 | .encrypt_add(&mongo_client, &dek) 61 | .await 62 | { 63 | Ok(session) => session, 64 | Err(e) => return Err(e), 65 | }; 66 | 67 | Ok(SignInOrSignUpResponse { 68 | message: "Signup successful".to_string(), 69 | uid: user.uid, 70 | name: user.name, 71 | email: user.email, 72 | role: user.role, 73 | created_at: user.created_at, 74 | updated_at: user.updated_at, 75 | email_verified: user.email_verified, 76 | is_active: user.is_active, 77 | session: SessionResponseForSignInOrSignUp { 78 | session_id: Encryption::encrypt_data(&session.session_id, &dek_data.dek), 79 | id_token: session.id_token, 80 | refresh_token: session.refresh_token, 81 | }, 82 | }) 83 | } 84 | 85 | pub async fn sign_in( 86 | mongo_client: &Client, 87 | email: &str, 88 | password: &str, 89 | user_agent: &str, 90 | ) -> Result { 91 | let user = match User::get_from_email(&mongo_client, email).await { 92 | Ok(user) => user, 93 | Err(e) => return Err(e), 94 | }; 95 | 96 | // check if the user has a blocked_until date greater than the current date check in milliseconds from DateTime type 97 | match user.blocked_until { 98 | Some(blocked_until_time) => { 99 | let current_time = DateTime::now().timestamp_millis(); 100 | if blocked_until_time.timestamp_millis() > current_time { 101 | return Err(Error::UserBlocked { 102 | message: "User is blocked".to_string(), 103 | }); 104 | } 105 | } 106 | None => {} 107 | } 108 | 109 | let dek_data = match Dek::get(&mongo_client, &user.uid).await { 110 | Ok(dek_data) => dek_data, 111 | Err(e) => return Err(e), 112 | }; 113 | 114 | // verify the password 115 | if Password::verify_hash(password, &user.password) { 116 | let session = match Session::new(&user, &user_agent) 117 | .encrypt_add(&mongo_client, &dek_data.dek) 118 | .await 119 | { 120 | Ok(session) => session, 121 | Err(e) => return Err(e), 122 | }; 123 | 124 | // make the failed login attempts to 0 125 | match User::reset_failed_login_attempt(&mongo_client, &user.email).await { 126 | Ok(_) => {} 127 | Err(e) => return Err(e), 128 | } 129 | 130 | let res = SignInOrSignUpResponse { 131 | message: "Signin successful".to_string(), 132 | uid: user.uid, 133 | name: user.name, 134 | email: user.email, 135 | role: user.role, 136 | created_at: user.created_at, 137 | updated_at: user.updated_at, 138 | email_verified: user.email_verified, 139 | is_active: user.is_active, 140 | session: SessionResponseForSignInOrSignUp { 141 | session_id: Encryption::encrypt_data(&session.session_id, &dek_data.dek), 142 | id_token: session.id_token, 143 | refresh_token: session.refresh_token, 144 | }, 145 | }; 146 | 147 | Ok(res) 148 | } else { 149 | match User::increase_failed_login_attempt(&mongo_client, &user.email).await { 150 | Ok(_) => {} 151 | Err(e) => return Err(e), 152 | } 153 | Err(Error::WrongCredentials { 154 | message: "Invalid credentials".to_string(), 155 | }) 156 | } 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /docs/local-setup/readme.md: -------------------------------------------------------------------------------- 1 | ## Local Setup Kubernets 2 | 3 | ### Step 1: Pre-requisites 4 | 5 | - [Rust Basics](https://doc.rust-lang.org/book/) 6 | - [Cargo (Rust package manager)](https://doc.rust-lang.org/cargo/getting-started/installation.html) 7 | - [Docker (For containerization)](https://docs.docker.com/get-docker/) 8 | - [Kubernets](https://kubernetes.io/) 9 | - [Minikube (For Kubernets)](https://minikube.sigs.k8s.io/docs/start/?arch=/macos/arm64/stable/binary+download) 10 | 11 | ### Step 2: Environment Variables 12 | 13 | Run this command to start setting up the environment👇 14 | ``` 15 | make setup 16 | ``` 17 | It will automatically start asking you all the required environment variables it needs and automatically create the environment variables it can. 18 | 19 | #### Here are some of the variables you need and here's how to get them 👇 20 | You can generate `SERVER_KEK` by running the command below from the root of your project. ( Make sure you have cargo installed ) 21 | 22 | ``` 23 | cargo run --bin create_kek 24 | 25 | ``` 26 | For testing purposes only you can use this SERVER_KEK as well: **9628177f62a03f5db4742273b915bf66.a21a897aa750** 27 | 28 | SMTP servers require authentication to ensure that only authorized users can send emails. For generating `EMAIL_PASSWORD`, Visit this [link](https://support.google.com/mail/thread/205453566/how-to-generate-an-app-password?hl=en). 29 | 30 | `SMTP_DOMAIN = smtp.gmail.com` as we are using GMAIL as a Mail Provider. 31 | 32 | For MongoDB username and password, you can use anything you want. But remember you need the same username and password for Mongo Compass mentioned in step 4. 33 | 34 | ### Step 3: Spinning up kubernetes 35 | 36 | Now it's time to spin up the kubernets cluster by running this following command (Make sure you have Docker installed) 37 | 38 | ``` 39 | make flexauth-up-k8s 40 | ``` 41 | 42 | This command will build all your local docker files using `skaffold` and then spin up the kubernets cluster. Once the pods are up and running it will start streamung the flexauth server logs to the terminal You can see the kubernetes deployment configs in the `k8s/local` file. 43 | 44 | **Tunneling** : If you want to tunnel the API and the Mongo express server make sure to run below command there so that you can reach the services by from your localhost. 45 | ``` 46 | minikube tunnel 47 | ``` 48 | 49 | Then you will be able to see your servers are running at the following addresses: 50 | 51 | **Flexauth server address:** `http://127.0.0.1:8080` 52 | 53 | **Mongo-express address:** `http://127.0.0.1:8081` 54 | 55 | Once done to shut down the cluster we need to run the command below 56 | ``` 57 | flexauth-down-k8s 58 | ``` 59 | 60 | **Note:** Killing the terminal that serving logs or minikube server doesn't make the kubernetes cluser down. So, make sure to run the command. 61 | 62 | Congrats, Your Local Setup is done successfully. 63 | 64 | 65 | ## Local Setup Docker 66 | 67 | ### Step 1: Pre-requisites 68 | 69 | - [Rust Basics](https://doc.rust-lang.org/book/) 70 | - [Cargo (Rust package manager)](https://doc.rust-lang.org/cargo/getting-started/installation.html) 71 | - [Docker (For containerization)](https://docs.docker.com/get-docker/) 72 | - [MongoDB Compass (For Visualizing DB with GUI)](https://www.mongodb.com/try/download/compass) 73 | 74 | ### Step 2: Environment Variables 75 | 76 | Run this command to start setting up the environment👇 77 | ``` 78 | make setup 79 | ``` 80 | It will automatically start asking you all the required environment variables it needs and automatically create the environment variables it can. 81 | 82 | #### Here are some of the variables you need and here's how to get them 👇 83 | You can generate `SERVER_KEK` by running the command below from the root of your project. ( Make sure you have cargo installed ) 84 | 85 | ``` 86 | cargo run --bin create_kek 87 | 88 | ``` 89 | For testing purposes only you can use this SERVER_KEK as well: **9628177f62a03f5db4742273b915bf66.a21a897aa750** 90 | 91 | SMTP servers require authentication to ensure that only authorized users can send emails. For generating `EMAIL_PASSWORD`, Visit this [link](https://support.google.com/mail/thread/205453566/how-to-generate-an-app-password?hl=en). 92 | `SMTP_DOMAIN = smtp.gmail.com` as we are using GMAIL as a Mail Provider. 93 | 94 | For MongoDB username and password, you can use anything you want. But remember you need the same username and password for Mongo Compass mentioned in step 4. 95 | 96 | ### Step 3: Spinning up Docker Containers 97 | 98 | Now it's time to run the docker container by running this following command (Make sure you have Docker installed) 99 | 100 | If you want to build the containers and then want to spin them up use this command 👇 101 | ``` 102 | flexauth-build-up-docker 103 | ``` 104 | 105 | Otherwise, you can use 106 | ``` 107 | make flexauth-up-docker 108 | ``` 109 | 110 | This command will start the container and watch the /src folder for any changes. If there are any modifications to the content inside /src, the container will automatically hot reload to reflect those changes. 111 | 112 | Note:- If there's any changes outside of the `/src` directory like- `cargo.toml` file, Make sure to stop the container and run the make command with `make build-run-server`. 113 | 114 | 115 | ### Step 4: Connecting to MongoDB Compass 116 | 117 | After running the Docker containers, you can connect to the MongoDB database using MongoDB Compass. Follow these steps: 118 | 119 | 1. **Open MongoDB Compass**: Launch MongoDB Compass on your system. 120 | 121 | 2. **Connect to a MongoDB Deployment**: 122 | - Click on the "New Connection" button to create a new connection. 123 | - In the "Connection String" field, paste the following URI: 124 | ```plaintext 125 | mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@localhost:27017/?directConnection=true&retryWrites=true&w=majority 126 | ``` 127 | - Replace the default URI with the one appropriate for your setup if necessary. This URI includes the credentials (`${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}`) from the .env and the default MongoDB port (`27017`). 128 | 129 | 3. **Connect to the Database**: 130 | - Click on the "Connect" button to establish a connection to the MongoDB deployment. 131 | - If the connection is successful, you will be able to browse and interact with the databases and collections in your MongoDB instance using MongoDB Compass. 132 | 133 | 4. **Explore Data**: You can now explore your MongoDB databases, collections, and documents, run queries, and perform other operations using MongoDB Compass. 134 | 135 | That's it! You are now connected to your MongoDB database using MongoDB Compass. 136 | 137 | ### Step 5: Running the UI: 138 | You just need to run this command 👇 139 | ``` 140 | make run-ui 141 | ``` 142 | 143 | Congrats, Your Local Setup is done successfully. 144 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | ################################################# Initial Setup ################################################# 2 | # Define the list of required environment variables for the root .env file 3 | REQUIRED_ENV_VARS = PORT SERVER_KEK EMAIL_PASSWORD EMAIL MAIL_NAME SMTP_DOMAIN SMTP_PORT MONGO_INITDB_ROOT_USERNAME MONGO_INITDB_ROOT_PASSWORD 4 | 5 | # Default target to check and update .env file 6 | .PHONY: setup 7 | setup: update-root-env check-private-key 8 | 9 | # Target to check and update the root .env file 10 | .PHONY: update-root-env 11 | update-root-env: 12 | @if [ -f .env ]; then \ 13 | echo ".env file exists."; \ 14 | else \ 15 | echo ".env file does not exist. Creating .env file...."; \ 16 | touch .env; \ 17 | fi; \ 18 | for var in $(REQUIRED_ENV_VARS); do \ 19 | if ! grep -q "^$${var}=" .env; then \ 20 | read -p "Enter value for $$var: " value; \ 21 | echo "$${var}=$$value" >> .env; \ 22 | else \ 23 | echo "✅ $${var}"; \ 24 | fi; \ 25 | done; \ 26 | if ! grep -q "^X_API_KEY=" .env; then \ 27 | X_API_KEY=$$(openssl rand -base64 32 | tr -d '='); \ 28 | echo "X_API_KEY=$$X_API_KEY" >> .env; \ 29 | echo "✅ Generated X_API_KEY=$$X_API_KEY"; \ 30 | else \ 31 | echo "✅ X_API_KEY"; \ 32 | fi 33 | 34 | 35 | # Target to check and generate private_key.pem if it doesn't exist 36 | .PHONY: check-private-key 37 | check-private-key: 38 | @if [ -f private_key.pem ]; then \ 39 | echo "🔑 private_key.pem exists."; \ 40 | else \ 41 | echo "private_key.pem does not exist. Generating private_key.pem."; \ 42 | openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048; \ 43 | echo "🔑 Generated private_key.pem"; \ 44 | fi 45 | 46 | ################################################# Docker Setups ################################################# 47 | 48 | # Build the Docker image for the server target dev only 49 | .PHONY: flexauth-build-docker 50 | build-server: check-private-key 51 | docker build -f Dockerfile . -t flexauth-server:dev --target dev 52 | 53 | # Target to run the server using Docker Compose without --build option 54 | .PHONY: flexauth-up-docker 55 | flexauth-up-docker: setup 56 | docker compose up 57 | 58 | # Target to run the server using Docker Compose with --build option 59 | .PHONY: flexauth-build-up-docker 60 | flexauth-build-up-docker: setup 61 | docker compose up --build 62 | 63 | ################################################# Kubernetes Setups ################################################# 64 | 65 | # Define the .env file and the skaffold files 66 | ENV_FILE := .env 67 | SKAFFOLD_TEMPLATE := skaffold.template.yaml 68 | SKAFFOLD_GENERATED := skaffold.generated.yaml 69 | NAMESPACE=flexauth 70 | SECRET=flexauth-secrets 71 | PROMETHEUS_RELEASE=prometheus 72 | GRAFANA_RELEASE=grafana 73 | 74 | # Load .env file and export all variables for Makefile 75 | include $(ENV_FILE) 76 | export $(shell sed 's/=.*//' $(ENV_FILE)) 77 | 78 | # Generate the skaffold.yaml file with envsubst 79 | $(SKAFFOLD_GENERATED): $(SKAFFOLD_TEMPLATE) 80 | @echo "Generating $(SKAFFOLD_GENERATED) with environment variables..." 81 | @envsubst '$$EMAIL $$EMAIL_PASSWORD $$MAIL_NAME $$SMTP_DOMAIN $$SMTP_PORT' < $(SKAFFOLD_TEMPLATE) > $(SKAFFOLD_GENERATED) 82 | @echo "$(SKAFFOLD_GENERATED) generated successfully." 83 | 84 | create-namespace: 85 | @echo "Creating namespace $(NAMESPACE)..." 86 | @if kubectl get namespace $(NAMESPACE) >/dev/null 2>&1; then \ 87 | echo "Namespace $(NAMESPACE) already exists."; \ 88 | else \ 89 | kubectl create namespace $(NAMESPACE) || (echo "Failed to create namespace." && exit 1); \ 90 | fi 91 | 92 | # Take envs from .env then encode them to base64 and create a secret in k8s using bash 93 | .PHONY: create-secret 94 | create-secret: 95 | @echo "Creating secret in k8s..." 96 | @if kubectl get secret $(SECRET) -n $(NAMESPACE) >/dev/null 2>&1; then \ 97 | echo "Secret $(SECRET) already exists. Overwriting..."; \ 98 | kubectl delete secret $(SECRET) -n $(NAMESPACE); \ 99 | fi && \ 100 | kubectl create secret generic $(SECRET) --from-env-file=.env -n $(NAMESPACE) || (echo "Failed to create secret." && exit 1) 101 | 102 | # Run Minikube 103 | .PHONY: minikube-up 104 | minikube-up: 105 | @echo "Running Skaffold..." 106 | @echo "Checking Minikube status..." 107 | @if minikube status | grep -q "host: Running"; then \ 108 | echo "Minikube is already running."; \ 109 | else \ 110 | echo "Starting Minikube..."; \ 111 | minikube start --driver=docker || (echo "Minikube failed to start." && exit 1); \ 112 | fi 113 | 114 | # Clean up generated files 115 | .PHONY: clean 116 | clean: 117 | @echo "Cleaning up generated files..." 118 | @rm -f $(SKAFFOLD_GENERATED) 119 | @echo "Clean-up complete." 120 | 121 | # Run flexauth using Skaffold and start tunneling with minikube but don't occupy the terminal 122 | .PHONY: flexauth-up-k8s 123 | up-k8s: 124 | @skaffold run -f $(SKAFFOLD_GENERATED) 125 | 126 | # start warching the logs of the flexauth server using kubectl 127 | .PHONY: flexauth-logs-k8s 128 | logs-k8s: 129 | @kubectl logs -n $(NAMESPACE) -l app=flexauth-server -f 130 | 131 | # Get the local address of the flexauth server and mongo-express server in minikube 132 | .PHONY: flexauth-address-k8s 133 | flexauth-address-k8s: 134 | @echo "Flexauth is running in minikube. Write "minikube tunnel" to start tunneling." 135 | @echo "Then you will be able to see your servers are running at the following addresses:" 136 | @echo "Flexauth server address: http://127.0.0.1:8080" 137 | @echo "Mongo-express address: http://127.0.0.1:8081" 138 | 139 | # Delete all the resources 140 | .PHONY: flexauth-down-k8s 141 | down-k8s: 142 | @echo "Deleting all resources..." 143 | @kubectl delete -f k8s/local 144 | @kubectl delete secret flexauth-secrets -n $(NAMESPACE) 145 | @kubectl delete namespace $(NAMESPACE) 146 | @echo "All resources deleted." 147 | 148 | .PHONY: up 149 | up-monitoring: create-namespace install-prometheus install-grafana 150 | 151 | create-namespace: 152 | kubectl create namespace $(NAMESPACE) || echo "Namespace $(NAMESPACE) already exists" 153 | 154 | install-prometheus: 155 | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 156 | helm repo update 157 | helm install $(PROMETHEUS_RELEASE) prometheus-community/prometheus --namespace $(NAMESPACE) 158 | 159 | install-grafana: 160 | helm repo add grafana https://grafana.github.io/helm-charts 161 | helm repo update 162 | helm install $(GRAFANA_RELEASE) grafana/grafana --namespace $(NAMESPACE) --set service.type=LoadBalancer 163 | 164 | .PHONY: down 165 | down-monitoring: uninstall-prometheus uninstall-grafana delete-namespace 166 | 167 | uninstall-prometheus: 168 | helm uninstall $(PROMETHEUS_RELEASE) --namespace $(NAMESPACE) || echo "$(PROMETHEUS_RELEASE) not installed" 169 | 170 | uninstall-grafana: 171 | helm uninstall $(GRAFANA_RELEASE) --namespace $(NAMESPACE) || echo "$(GRAFANA_RELEASE) not installed" 172 | 173 | delete-namespace: 174 | kubectl delete namespace $(NAMESPACE) || echo "Namespace $(NAMESPACE) already deleted" 175 | 176 | # Final targets 177 | flexauth-up-k8s: setup minikube-up create-namespace create-secret $(SKAFFOLD_GENERATED) up-k8s up-monitoring clean logs-k8s 178 | flexauth-down-k8s: down-k8s down-monitoring clean 179 | 180 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | http::StatusCode, 3 | response::{IntoResponse, Response}, 4 | }; 5 | use serde::Serialize; 6 | 7 | pub type Result = core::result::Result; 8 | 9 | #[derive(Clone, Debug, strum_macros::AsRefStr, Serialize)] 10 | #[serde(tag = "type", content = "data")] 11 | pub enum Error { 12 | // -- Model errors 13 | InvalidPayload { message: String }, 14 | 15 | // -- User Errors 16 | UserNotFound { message: String }, 17 | UserAlreadyExists { message: String }, 18 | WrongCredentials { message: String }, 19 | UserBlocked { message: String }, 20 | 21 | // -- Password Errors 22 | InvalidPassword { message: String }, 23 | ResetPasswordLinkExpired { message: String }, 24 | 25 | // -- Session Errors 26 | InvalidToken { message: String }, 27 | RefreshTokenCreationError { message: String }, 28 | IdTokenCreationError { message: String }, 29 | PublicKeyLoadError { message: String }, 30 | PrivateKeyLoadError { message: String }, 31 | SignatureVerificationError { message: String }, 32 | ExpiredSignature { message: String }, 33 | SessionExpired { message: String }, 34 | ActiveSessionExists { message: String }, 35 | SessionNotFound { message: String }, 36 | 37 | // -- Email erros 38 | EmailVerificationLinkExpired { message: String }, 39 | BlockRequestLinkExpired { message: String }, 40 | 41 | // -- Validation Errors 42 | InvalidEmail { message: String }, 43 | InvalidUserAgent { message: String }, 44 | 45 | // -- Encryption Errors 46 | KeyNotFound { message: String }, 47 | 48 | ServerError { message: String }, 49 | } 50 | 51 | impl IntoResponse for Error { 52 | fn into_response(self) -> Response { 53 | println!("{:?}", self); 54 | 55 | // Create a placeholder Axum response 56 | let mut response = StatusCode::INTERNAL_SERVER_ERROR.into_response(); 57 | 58 | // Insert the error message into the response body 59 | response.extensions_mut().insert(self); 60 | 61 | response 62 | } 63 | } 64 | 65 | impl Error { 66 | pub fn client_status_and_error(&self) -> (StatusCode, ClientError) { 67 | #[allow(unreachable_patterns)] 68 | match self { 69 | // -- Model errors 70 | Self::InvalidPayload { message: _ } => { 71 | (StatusCode::BAD_REQUEST, ClientError::INVALID_PARAMS) 72 | } 73 | 74 | // -- User Errors 75 | Self::UserNotFound { message: _ } => { 76 | (StatusCode::NOT_FOUND, ClientError::USER_NOT_FOUND) 77 | } 78 | 79 | Self::UserAlreadyExists { message: _ } => { 80 | (StatusCode::FOUND, ClientError::USER_ALREADY_EXISTS) 81 | } 82 | 83 | // -- Validation Errors 84 | Self::InvalidEmail { message: _ } => { 85 | (StatusCode::BAD_REQUEST, ClientError::INVALID_PARAMS) 86 | } 87 | 88 | Self::InvalidUserAgent { message: _ } => { 89 | (StatusCode::BAD_REQUEST, ClientError::INVALID_PARAMS) 90 | } 91 | 92 | // -- Password Errors 93 | Self::InvalidPassword { message: _ } => { 94 | (StatusCode::UNAUTHORIZED, ClientError::INVALID_PASSWORD) 95 | } 96 | 97 | Self::WrongCredentials { message: _ } => { 98 | (StatusCode::UNAUTHORIZED, ClientError::WRONG_CREDENTIALS) 99 | } 100 | 101 | Self::UserBlocked { message: _ } => { 102 | (StatusCode::UNAUTHORIZED, ClientError::USER_BLOCKED) 103 | } 104 | 105 | Self::KeyNotFound { message: _ } => ( 106 | StatusCode::INTERNAL_SERVER_ERROR, 107 | ClientError::SERVICE_ERROR, 108 | ), 109 | 110 | Self::ResetPasswordLinkExpired { message: _ } => ( 111 | StatusCode::UNAUTHORIZED, 112 | ClientError::RESET_PASSWORD_LINK_EXPIRED, 113 | ), 114 | 115 | // -- Session Errors 116 | Self::PublicKeyLoadError { message: _ } => ( 117 | StatusCode::INTERNAL_SERVER_ERROR, 118 | ClientError::SERVICE_ERROR, 119 | ), 120 | 121 | Self::PrivateKeyLoadError { message: _ } => ( 122 | StatusCode::INTERNAL_SERVER_ERROR, 123 | ClientError::SERVICE_ERROR, 124 | ), 125 | 126 | Self::SignatureVerificationError { message: _ } => ( 127 | StatusCode::UNAUTHORIZED, 128 | ClientError::SIGNATURE_VERIFICATION_ERROR, 129 | ), 130 | 131 | Self::ExpiredSignature { message: _ } => { 132 | (StatusCode::UNAUTHORIZED, ClientError::EXPIRED_SIGNATURE) 133 | } 134 | 135 | Self::InvalidToken { message: _ } => { 136 | (StatusCode::UNAUTHORIZED, ClientError::INVALID_TOKEN) 137 | } 138 | Self::IdTokenCreationError { message: _ } => ( 139 | StatusCode::INTERNAL_SERVER_ERROR, 140 | ClientError::SERVICE_ERROR, 141 | ), 142 | 143 | Self::RefreshTokenCreationError { message: _ } => ( 144 | StatusCode::INTERNAL_SERVER_ERROR, 145 | ClientError::SERVICE_ERROR, 146 | ), 147 | 148 | Self::SessionExpired { message: _ } => { 149 | (StatusCode::UNAUTHORIZED, ClientError::SESSION_EXPIRED) 150 | } 151 | 152 | Self::ServerError { message: _ } => ( 153 | StatusCode::INTERNAL_SERVER_ERROR, 154 | ClientError::SERVICE_ERROR, 155 | ), 156 | 157 | Self::ActiveSessionExists { message: _ } => { 158 | (StatusCode::CONFLICT, ClientError::ACTIVE_SESSION_EXISTS) 159 | } 160 | 161 | Self::SessionNotFound { message: _ } => { 162 | (StatusCode::NOT_FOUND, ClientError::SESSION_NOT_FOUND) 163 | } 164 | 165 | // -- Email errors 166 | Self::EmailVerificationLinkExpired { message: _ } => { 167 | (StatusCode::UNAUTHORIZED, ClientError::EMAIL_VERIFICATION_LINK_EXPIRED) 168 | } 169 | 170 | Self::BlockRequestLinkExpired { message: _ } => { 171 | (StatusCode::UNAUTHORIZED, ClientError::BLOCK_REQUEST_LINK_EXPIRED) 172 | } 173 | 174 | _ => ( 175 | StatusCode::INTERNAL_SERVER_ERROR, 176 | ClientError::SERVICE_ERROR, 177 | ), 178 | } 179 | } 180 | } 181 | 182 | #[derive(Debug, strum_macros::AsRefStr)] 183 | #[allow(non_camel_case_types)] 184 | pub enum ClientError { 185 | USER_NOT_FOUND, 186 | INVALID_PARAMS, 187 | SERVICE_ERROR, 188 | USER_ALREADY_EXISTS, 189 | INVALID_PASSWORD, 190 | WRONG_CREDENTIALS, 191 | USER_BLOCKED, 192 | RESET_PASSWORD_LINK_EXPIRED, 193 | INVALID_TOKEN, 194 | SIGNATURE_VERIFICATION_ERROR, 195 | EXPIRED_SIGNATURE, 196 | SESSION_EXPIRED, 197 | ACTIVE_SESSION_EXISTS, 198 | SESSION_NOT_FOUND, 199 | EMAIL_VERIFICATION_LINK_EXPIRED, 200 | BLOCK_REQUEST_LINK_EXPIRED, 201 | } 202 | 203 | // region: --- Error Boilerplate 204 | impl core::fmt::Display for Error { 205 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> { 206 | write!(fmt, "{self:?}") 207 | } 208 | } 209 | 210 | impl std::error::Error for Error {} 211 | // end region: --- Error Boilerplate 212 | -------------------------------------------------------------------------------- /src/handlers/session_handler.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::State, 3 | http::{header, HeaderMap}, 4 | Json, 5 | }; 6 | use axum_macros::debug_handler; 7 | 8 | use crate::{ 9 | core::session::Session, 10 | errors::{Error, Result}, 11 | models::{ 12 | session_model::{ 13 | DeleteAllSessionsPayload, DeleteAllSessionsResult, DeleteSessionsPayload, DeleteSessionsResult, RevokeAllSessionsPayload, RevokeAllSessionsResult, RevokeSessionsPayload, RevokeSessionsResult, SessionDetailsPayload, SessionRefreshPayload, SessionRefreshResult, SessionResponse, VerifySession 14 | }, 15 | user_model::UserIdPayload, 16 | }, 17 | utils::session_utils::IDToken, 18 | AppState, 19 | }; 20 | 21 | #[debug_handler] 22 | pub async fn verify_session_handler( 23 | State(state): State, 24 | payload: Json, 25 | ) -> Result> { 26 | // check if the token is not empty 27 | if payload.token.is_empty() { 28 | return Err(Error::InvalidPayload { 29 | message: "Invalid payload passed".to_string(), 30 | }); 31 | } 32 | 33 | // verify the token 34 | match Session::verify(&state.mongo_client, &payload.token).await { 35 | Ok(data) => { 36 | return { 37 | if data.1 { 38 | Ok(Json(data.0)) 39 | } else { 40 | Err(Error::SessionExpired { 41 | message: "Session expired".to_string(), 42 | }) 43 | } 44 | } 45 | } 46 | Err(e) => return Err(e), 47 | }; 48 | } 49 | 50 | #[debug_handler] 51 | pub async fn get_all_handler( 52 | State(state): State, 53 | ) -> Result>> { 54 | // verify the token 55 | match Session::get_all(&state.mongo_client).await { 56 | Ok(data) => { 57 | return Ok(Json(data)); 58 | } 59 | Err(e) => return Err(e), 60 | }; 61 | } 62 | 63 | #[debug_handler] 64 | pub async fn get_all_from_uid_handler( 65 | State(state): State, 66 | payload: Json, 67 | ) -> Result>> { 68 | // check if the token is not empty 69 | if payload.uid.is_empty() { 70 | return Err(Error::InvalidPayload { 71 | message: "Invalid payload passed".to_string(), 72 | }); 73 | } 74 | 75 | // verify the token 76 | match Session::get_all_from_uid(&state.mongo_client, &payload.uid).await { 77 | Ok(data) => { 78 | return Ok(Json(data)); 79 | } 80 | Err(e) => return Err(e), 81 | }; 82 | } 83 | 84 | #[debug_handler] 85 | pub async fn get_details_handler( 86 | State(state): State, 87 | payload: Json, 88 | ) -> Result> { 89 | // check if the token is not empty 90 | if payload.uid.is_empty() | payload.session_id.is_empty() { 91 | return Err(Error::InvalidPayload { 92 | message: "Invalid payload passed".to_string(), 93 | }); 94 | } 95 | 96 | match Session::get_details(&state.mongo_client, &payload.uid, &payload.session_id).await { 97 | Ok(data) => { 98 | return Ok(Json(data)); 99 | } 100 | Err(e) => return Err(e), 101 | }; 102 | } 103 | 104 | #[debug_handler] 105 | pub async fn refresh_session_handler( 106 | State(state): State, 107 | header: HeaderMap, 108 | payload: Json, 109 | ) -> Result> { 110 | // check if the token is not empty 111 | if payload.id_token.is_empty() 112 | || payload.refresh_token.is_empty() 113 | || payload.session_id.is_empty() 114 | || payload.uid.is_empty() 115 | { 116 | return Err(Error::InvalidPayload { 117 | message: "Invalid payload passed".to_string(), 118 | }); 119 | } 120 | 121 | // get user-agent form the header 122 | let user_agent = match header.get(header::USER_AGENT) { 123 | Some(ua) => ua.to_str().unwrap().to_string(), 124 | None => "".to_string(), 125 | }; 126 | 127 | if user_agent.is_empty() { 128 | return Err(Error::InvalidUserAgent { 129 | message: "Invalid User Agent, Can't let random user to signin".to_string(), 130 | }); 131 | } 132 | 133 | // verify the token 134 | match Session::refresh( 135 | &state.mongo_client, 136 | &payload.uid, 137 | &payload.session_id, 138 | &payload.id_token, 139 | &payload.refresh_token, 140 | &user_agent, 141 | ) 142 | .await 143 | { 144 | Ok(data) => { 145 | return Ok(Json(SessionRefreshResult { 146 | uid: payload.uid.clone(), 147 | session_id: payload.session_id.clone(), 148 | id_token: data.0, 149 | refresh_token: data.1, 150 | })); 151 | } 152 | Err(e) => return Err(e), 153 | }; 154 | } 155 | 156 | #[debug_handler] 157 | pub async fn revoke_handler( 158 | State(state): State, 159 | payload: Json, 160 | ) -> Result> { 161 | // check if the token is not empty 162 | if payload.session_id.is_empty() | payload.uid.is_empty() { 163 | return Err(Error::InvalidPayload { 164 | message: "Invalid payload passed".to_string(), 165 | }); 166 | } 167 | 168 | // revoke the session 169 | match Session::revoke(&state.mongo_client, &payload.session_id, &payload.uid).await { 170 | Ok(_) => { 171 | return Ok(Json(RevokeSessionsResult { 172 | message: "Session revoked successfully".to_string(), 173 | })) 174 | } 175 | Err(e) => return Err(e), 176 | }; 177 | } 178 | 179 | #[debug_handler] 180 | pub async fn revoke_all_handler( 181 | State(state): State, 182 | payload: Json, 183 | ) -> Result> { 184 | // revoke all the sessions 185 | match Session::revoke_all(&state.mongo_client, &payload.uid).await { 186 | Ok(_) => { 187 | return Ok(Json(RevokeAllSessionsResult { 188 | message: "All sessions revoked successfully".to_string(), 189 | })) 190 | } 191 | Err(e) => return Err(e), 192 | }; 193 | } 194 | 195 | #[debug_handler] 196 | pub async fn delete_handler( 197 | State(state): State, 198 | 199 | payload: Json, 200 | ) -> Result> { 201 | // revoke all the sessions 202 | if payload.session_id.is_empty() | payload.uid.is_empty(){ 203 | return Err(Error::InvalidPayload { 204 | message: "Invalid payload passed".to_string(), 205 | }); 206 | } 207 | 208 | match Session::delete(&state.mongo_client, &payload.session_id, &payload.uid).await { 209 | Ok(_) => { 210 | return Ok(Json(DeleteSessionsResult { 211 | message: "Session deleted successfully".to_string(), 212 | })) 213 | } 214 | Err(e) => return Err(e), 215 | }; 216 | } 217 | 218 | #[debug_handler] 219 | pub async fn delete_all_handler( 220 | State(state): State, 221 | payload: Json, 222 | ) -> Result> { 223 | // revoke all the sessions 224 | if payload.uid.is_empty() { 225 | return Err(Error::InvalidPayload { 226 | message: "Invalid payload passed".to_string(), 227 | }); 228 | } 229 | 230 | match Session::delete_all(&state.mongo_client, &payload.uid).await { 231 | Ok(_) => { 232 | return Ok(Json(DeleteAllSessionsResult { 233 | message: "All sessions deleted successfully".to_string(), 234 | })) 235 | } 236 | Err(e) => return Err(e), 237 | }; 238 | } 239 | -------------------------------------------------------------------------------- /src/handlers/password_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | core::user::User, 3 | errors::{Error, Result}, 4 | models::password_model::{ 5 | ForgetPasswordPayload, ForgetPasswordResetPayload, ResetPasswordPayload, 6 | }, 7 | utils::validation_utils::Validation, 8 | AppState, 9 | }; 10 | use axum::{ 11 | extract::{Path, State}, response::{Html, IntoResponse}, Json 12 | }; 13 | use axum_macros::debug_handler; 14 | use bson::doc; 15 | use serde_json::{json, Value}; 16 | 17 | #[debug_handler] 18 | pub async fn reset_password_handler( 19 | State(state): State, 20 | payload: Json, 21 | ) -> Result> { 22 | // check if payload is valid 23 | if payload.email.is_empty() | payload.old_password.is_empty() | payload.new_password.is_empty() 24 | { 25 | return Err(Error::InvalidPayload { 26 | message: "Email and password are required.".to_string(), 27 | }); 28 | } 29 | 30 | if !Validation::password(&payload.new_password) || !Validation::password(&payload.old_password) 31 | { 32 | return Err(Error::InvalidPayload { 33 | message: "Password must be at least 8 characters long.".to_string(), 34 | }); 35 | } 36 | 37 | match User::change_password( 38 | &state.mongo_client, 39 | &payload.email, 40 | &payload.old_password, 41 | &payload.new_password, 42 | ) 43 | .await 44 | { 45 | Ok(_) => { 46 | return Ok(Json(json!({ 47 | "message": "Password updated successfully. Please login with the new password." 48 | }))); 49 | } 50 | Err(e) => return Err(e), 51 | } 52 | } 53 | 54 | #[debug_handler] 55 | pub async fn forget_password_request_handler( 56 | State(state): State, 57 | payload: Json, 58 | ) -> Result> { 59 | // check if payload.email exists 60 | if payload.email.is_empty() { 61 | return Err(Error::InvalidPayload { 62 | message: "Email is required.".to_string(), 63 | }); 64 | } 65 | 66 | match User::forget_password_request(&state.mongo_client, &payload.email).await { 67 | Ok(_) => { 68 | return Ok(Json(json!({ 69 | "message": "Password reset request sent successfully. Please check your email." 70 | }))); 71 | } 72 | Err(e) => return Err(e), 73 | } 74 | } 75 | 76 | #[debug_handler] 77 | pub async fn forget_password_reset_handler( 78 | Path(id): Path, 79 | State(state): State, 80 | payload: Json, 81 | ) -> Result> { 82 | // check if payload is valid 83 | if payload.email.is_empty() | payload.password.is_empty() { 84 | return Err(Error::InvalidPayload { 85 | message: "Invalid Payload".to_string(), 86 | }); 87 | } 88 | match User::forget_password_reset(&state.mongo_client, &id, &payload.email, &payload.password) 89 | .await 90 | { 91 | Ok(_) => { 92 | return Ok(Json(json!({ 93 | "message": "Password updated successfully. Please login with the new password." 94 | }))); 95 | } 96 | Err(e) => return Err(e), 97 | } 98 | } 99 | 100 | #[debug_handler] 101 | pub async fn forget_password_form(Path(id): Path) -> impl IntoResponse { 102 | Html(format!(r#" 103 | 104 | 105 | 106 | 107 | 108 | Reset Password 109 | 123 | 124 | 125 | 128 |

Reset Password

129 |
130 |
131 | 132 | 133 | 134 | 135 | 136 | 137 |
138 | 139 |

Note: Password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one number, and one special character.

140 |
141 |
142 | 191 | 192 | 193 | "#, id = id, api_key = dotenv::var("X_API_KEY").unwrap())) 194 | } 195 | 196 | -------------------------------------------------------------------------------- /src/utils/session_utils.rs: -------------------------------------------------------------------------------- 1 | use jsonwebtoken as jwt; 2 | use jwt::{DecodingKey, EncodingKey, Header, Validation}; 3 | use openssl::pkey::PKey; 4 | use openssl::rsa::Rsa; 5 | use serde::{Deserialize, Serialize}; 6 | use std::{collections::HashMap, env, fs}; 7 | 8 | use crate::{core::user::User, errors::Error}; 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | pub struct IDToken { 12 | pub uid: String, 13 | iss: String, 14 | iat: usize, 15 | exp: usize, 16 | token_type: String, 17 | pub data: Option>, 18 | } 19 | 20 | pub fn load_private_key() -> Result, Error> { 21 | let private_key_content = fs::read("private_key.pem"); 22 | let rsa = Rsa::private_key_from_pem(&private_key_content.unwrap()).unwrap(); 23 | let private_key = PKey::from_rsa(rsa).unwrap(); 24 | match private_key.private_key_to_pem_pkcs8() { 25 | Ok(key) => return Ok(key), 26 | Err(_) => { 27 | return Err(Error::PrivateKeyLoadError { 28 | message: "Error loading private key".to_string(), 29 | }); 30 | } 31 | }; 32 | } 33 | 34 | // Load public key from the private key 35 | pub fn load_public_key() -> Result, Error> { 36 | let private_key_content = fs::read("private_key.pem"); 37 | let rsa = Rsa::private_key_from_pem(&private_key_content.unwrap()).unwrap(); 38 | let private_key = PKey::from_rsa(rsa).unwrap(); 39 | match private_key.public_key_to_pem() { 40 | Ok(key) => return Ok(key), 41 | Err(_) => { 42 | return Err(Error::PublicKeyLoadError { 43 | message: "Error deriving public key from private key".to_string(), 44 | }); 45 | } 46 | }; 47 | } 48 | 49 | impl IDToken { 50 | pub fn new(user: &User) -> Self { 51 | let server_url = 52 | env::var("SERVER_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); 53 | Self { 54 | uid: user.uid.to_string(), 55 | iss: server_url, 56 | iat: chrono::Utc::now().timestamp() as usize, 57 | exp: chrono::Utc::now().timestamp() as usize + 3600, // 1h 58 | token_type: "id".to_string(), 59 | data : Some( 60 | [ 61 | ("display_name".to_string(), user.name.to_string()), 62 | ("role".to_string(), user.role.to_string()), 63 | ("is_active".to_string(), user.is_active.to_string()), 64 | ( 65 | "is_email_verified".to_string(), 66 | user.email_verified.to_string(), 67 | ), 68 | ] 69 | .iter() 70 | .cloned() 71 | .collect(), 72 | ), 73 | } 74 | } 75 | 76 | pub fn sign(&self) -> Result { 77 | let private_key = load_private_key()?; 78 | let header = Header::new(jwt::Algorithm::RS256); 79 | let encoding_key = match EncodingKey::from_rsa_pem(&private_key) { 80 | Ok(key) => key, 81 | Err(err) => { 82 | eprintln!("Error creating decoding key: {}", err); 83 | return Err(Error::PublicKeyLoadError { 84 | message: (err.to_string()), 85 | }); 86 | } 87 | }; 88 | 89 | match jwt::encode(&header, &self, &encoding_key) { 90 | Ok(token) => return Ok(token), 91 | Err(err) => { 92 | return Err(Error::IdTokenCreationError { 93 | message: err.to_string(), 94 | }) 95 | } 96 | }; 97 | } 98 | 99 | pub fn verify(token: &str) -> Result<(Self, bool), Error> { 100 | let public_key = load_public_key()?; 101 | let validation = Validation::new(jwt::Algorithm::RS256); 102 | // Try to create a DecodingKey from the public key 103 | let decoding_key = match DecodingKey::from_rsa_pem(&public_key) { 104 | Ok(key) => key, 105 | Err(err) => { 106 | eprintln!("Error creating decoding key: {}", err); 107 | return Err(Error::PublicKeyLoadError { 108 | message: (err.to_string()), 109 | }); 110 | } 111 | }; 112 | // return false if the token is not valid 113 | match jwt::decode::(&token, &decoding_key, &validation) { 114 | Ok(val) => { 115 | let token_data = val.claims; 116 | Ok((token_data, true)) 117 | } 118 | Err(e) => match e.kind() { 119 | // check if ExpiredSignature 120 | jwt::errors::ErrorKind::ExpiredSignature => { 121 | // get token claims even if it is expired to check the data by decoding it with exp flag set to false 122 | let mut validation = Validation::new(jwt::Algorithm::RS256); 123 | validation.validate_exp = false; 124 | match jwt::decode::(&token, &decoding_key, &validation) { 125 | Ok(val) => { 126 | let token_data = val.claims; 127 | Ok((token_data, false)) 128 | } 129 | Err(_) => { 130 | return Err(Error::ServerError { 131 | message: "Error decoding token".to_string(), 132 | }) 133 | } 134 | } 135 | } 136 | // check if InvalidSignature 137 | jwt::errors::ErrorKind::InvalidSignature => { 138 | return Err(Error::SignatureVerificationError { 139 | message: "Invalid signature".to_string(), 140 | }) 141 | } 142 | _ => { 143 | return Err(Error::InvalidToken { 144 | message: "Invalid token".to_string(), 145 | }) 146 | } 147 | }, 148 | } 149 | } 150 | 151 | } 152 | 153 | // RefreshToken struct 154 | #[derive(Debug, Serialize, Deserialize)] 155 | pub struct RefreshToken { 156 | pub uid: String, 157 | iss: String, 158 | iat: usize, 159 | exp: usize, 160 | scope: String, 161 | pub data: Option>, 162 | } 163 | 164 | impl RefreshToken { 165 | pub fn new(uid: &str) -> Self { 166 | let server_url = 167 | std::env::var("SERVER_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); 168 | Self { 169 | uid: uid.to_string(), 170 | iss: server_url, 171 | iat: chrono::Utc::now().timestamp() as usize, 172 | exp: chrono::Utc::now().timestamp() as usize + (3600 * 24 * 45), // 45 days 173 | scope: "get_new_id_token".to_string(), 174 | data: None, 175 | } 176 | } 177 | 178 | pub fn sign(&self) -> Result { 179 | let private_key = load_private_key()?; 180 | let header = Header::new(jwt::Algorithm::RS256); 181 | let encoding_key = match EncodingKey::from_rsa_pem(&private_key) { 182 | Ok(key) => key, 183 | Err(err) => { 184 | eprintln!("Error creating decoding key: {}", err); 185 | return Err(Error::PublicKeyLoadError { 186 | message: (err.to_string()), 187 | }); 188 | } 189 | }; 190 | 191 | match jwt::encode(&header, &self, &encoding_key) { 192 | Ok(token) => return Ok(token), 193 | Err(err) => { 194 | return Err(Error::RefreshTokenCreationError { 195 | message: err.to_string(), 196 | }) 197 | } 198 | }; 199 | } 200 | 201 | pub fn verify(token: &str) -> Result { 202 | let public_key = load_public_key()?; 203 | let validation = Validation::new(jwt::Algorithm::RS256); 204 | // Try to create a DecodingKey from the public key 205 | let decoding_key = match DecodingKey::from_rsa_pem(&public_key) { 206 | Ok(key) => key, 207 | Err(err) => { 208 | eprintln!("Error creating decoding key: {}", err); 209 | return Err(Error::PublicKeyLoadError { 210 | message: (err.to_string()), 211 | }); 212 | } 213 | }; 214 | // return false if the token is not valid 215 | match jwt::decode::(&token, &decoding_key, &validation) { 216 | Ok(val) => { 217 | let token_data = val.claims; 218 | Ok(token_data) 219 | } 220 | Err(e) => match e { 221 | // check if ExpiredSignature 222 | _ if e.to_string().contains("ExpiredSignature") => { 223 | return Err(Error::ExpiredSignature { 224 | message: "Expired signature".to_string(), 225 | }) 226 | } 227 | // check if InvalidSignature 228 | _ if e.to_string().contains("InvalidSignature") => { 229 | return Err(Error::SignatureVerificationError { 230 | message: "Invalid signature".to_string(), 231 | }) 232 | } 233 | _ => { 234 | return Err(Error::InvalidToken { 235 | message: "Invalid token".to_string(), 236 | }) 237 | } 238 | }, 239 | } 240 | } 241 | } -------------------------------------------------------------------------------- /src/handlers/user_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | core::{dek::Dek, session::Session, user::User}, 3 | errors::{Error, Result}, 4 | models::user_model::{ 5 | BlockUserResponse, EmailVerificationResponse, RecentUserPayload, ToggleUserActivationStatusPayload, ToggleUserActivationStatusResponse, UpdateUserPayload, UpdateUserResponse, UpdateUserRolePayload, UpdateUserRoleResponse, UserEmailPayload, UserEmailResponse, UserIdPayload, UserResponse 6 | }, 7 | utils::{encryption_utils::Encryption, validation_utils::Validation}, 8 | AppState, 9 | }; 10 | use axum::{ 11 | extract::{Path, State}, 12 | response::{Html, IntoResponse}, 13 | Json, 14 | }; 15 | use axum_macros::debug_handler; 16 | use bson::{doc, DateTime}; 17 | use mongodb::Collection; 18 | 19 | pub async fn get_all_users_handler( 20 | State(state): State, 21 | ) -> Result>> { 22 | println!(">> HANDLER: get_user_handler called"); 23 | 24 | match User::get_all(&state.mongo_client).await { 25 | Ok(users) => Ok(Json(users)), 26 | Err(e) => Err(e), 27 | } 28 | } 29 | 30 | pub async fn get_recent_users_handler( 31 | State(state): State, 32 | payload: Json, 33 | ) -> Result>> { 34 | println!(">> HANDLER: get_recent_users_handler called"); 35 | 36 | match User::get_recent(&state.mongo_client, payload.limit).await { 37 | Ok(users) => Ok(Json(users)), 38 | Err(e) => Err(e), 39 | } 40 | } 41 | 42 | pub async fn update_user_handler( 43 | State(state): State, 44 | payload: Json, 45 | ) -> Result> { 46 | println!(">> HANDLER: update_user_handler called"); 47 | 48 | // check if the payload is empty 49 | if payload.email.is_empty() || payload.name.is_empty() { 50 | return Err(Error::InvalidPayload { 51 | message: "Invalid payload".to_string(), 52 | }); 53 | } 54 | 55 | if !Validation::email(&payload.email) { 56 | return Err(Error::InvalidPayload { 57 | message: "Invalid Email".to_string(), 58 | }); 59 | } 60 | 61 | let db = state.mongo_client.database("auth"); 62 | let collection: Collection = db.collection("users"); 63 | let dek_data = match Dek::get(&state.mongo_client, &payload.email).await { 64 | Ok(dek) => dek, 65 | Err(e) => return Err(e), 66 | }; 67 | // find the user in the users collection using the uid 68 | match collection 69 | .update_one( 70 | doc! { 71 | "uid": &dek_data.uid, 72 | }, 73 | doc! { 74 | "$set": { 75 | "name": Encryption::encrypt_data(&payload.name, &dek_data.dek), 76 | "updated_at": DateTime::now(), 77 | } 78 | }, 79 | None, 80 | ) 81 | .await 82 | { 83 | Ok(res) => { 84 | if res.modified_count == 0 { 85 | return Err(Error::UserNotFound { 86 | message: "User not found".to_string(), 87 | }); 88 | } 89 | Ok(Json(UpdateUserResponse { 90 | email: payload.email.to_owned(), 91 | name: payload.name.to_owned(), 92 | })) 93 | } 94 | Err(e) => { 95 | return Err(Error::ServerError { 96 | message: e.to_string(), 97 | }) 98 | } 99 | } 100 | } 101 | 102 | pub async fn update_user_role_handler( 103 | State(state): State, 104 | payload: Json, 105 | ) -> Result> { 106 | println!(">> HANDLER: update_user_role_handler called"); 107 | 108 | // check if the payload is empty 109 | if payload.email.is_empty() || payload.role.is_empty() { 110 | return Err(Error::InvalidPayload { 111 | message: "Invalid payload".to_string(), 112 | }); 113 | } 114 | 115 | if !Validation::email(&payload.email) { 116 | return Err(Error::InvalidPayload { 117 | message: "Invalid Email".to_string(), 118 | }); 119 | } 120 | 121 | match User::update_role(&State(state).mongo_client, &payload.email, &payload.role).await { 122 | Ok(role) => { 123 | return Ok(Json(UpdateUserRoleResponse { 124 | message: "User role updated".to_string(), 125 | email: payload.email.to_owned(), 126 | role, 127 | })) 128 | } 129 | Err(e) => return Err(e), 130 | } 131 | } 132 | 133 | pub async fn toggle_user_activation_status( 134 | State(state): State, 135 | payload: Json, 136 | ) -> Result> { 137 | println!(">> HANDLER: update_user_role_handler called"); 138 | 139 | match payload.is_active { 140 | Some(_) => { 141 | if payload.email.is_empty() { 142 | return Err(Error::InvalidPayload { 143 | message: "Invalid payload".to_string(), 144 | }); 145 | } 146 | } 147 | None => { 148 | return Err(Error::InvalidPayload { 149 | message: "Invalid payload".to_string(), 150 | }); 151 | } 152 | } 153 | 154 | match User::toggle_account_activation( 155 | &State(state).mongo_client, 156 | &payload.email, 157 | &payload.is_active.unwrap(), 158 | ) 159 | .await 160 | { 161 | Ok(is_active_final) => { 162 | return Ok(Json(ToggleUserActivationStatusResponse { 163 | message: "User activation status updated".to_string(), 164 | email: payload.email.to_owned(), 165 | is_active: is_active_final, 166 | })) 167 | } 168 | Err(e) => return Err(e), 169 | } 170 | } 171 | 172 | #[debug_handler] 173 | pub async fn get_user_email_handler( 174 | State(state): State, 175 | payload: Json, 176 | ) -> Result> { 177 | println!(">> HANDLER: get_user_by_email_handler called"); 178 | 179 | if !Validation::email(&payload.email) { 180 | return Err(Error::InvalidPayload { 181 | message: "Invalid Email".to_string(), 182 | }); 183 | } 184 | 185 | match User::get_from_email(&state.mongo_client, &payload.email).await { 186 | Ok(user) => { 187 | return Ok(Json(UserResponse { 188 | uid: user.uid, 189 | email: user.email, 190 | name: user.name, 191 | role: user.role, 192 | is_active: user.is_active, 193 | email_verified: user.email_verified, 194 | blocked_until: user.blocked_until, 195 | created_at: user.created_at, 196 | updated_at: user.updated_at, 197 | })) 198 | } 199 | Err(e) => return Err(e), 200 | } 201 | } 202 | 203 | #[debug_handler] 204 | pub async fn get_user_id_handler( 205 | State(state): State, 206 | payload: Json, 207 | ) -> Result> { 208 | println!(">> HANDLER: get_user_by_id handler called"); 209 | if payload.uid.is_empty() { 210 | return Err(Error::InvalidPayload { 211 | message: "Invalid payload".to_string(), 212 | }); 213 | } 214 | 215 | match User::get_from_uid(&state.mongo_client, &payload.uid).await { 216 | Ok(user) => { 217 | return Ok(Json(UserResponse { 218 | uid: user.uid, 219 | email: user.email, 220 | name: user.name, 221 | role: user.role, 222 | is_active: user.is_active, 223 | email_verified: user.email_verified, 224 | blocked_until: user.blocked_until, 225 | created_at: user.created_at, 226 | updated_at: user.updated_at, 227 | })) 228 | } 229 | Err(e) => return Err(e), 230 | } 231 | } 232 | 233 | #[debug_handler] 234 | pub async fn verify_email_request_handler( 235 | State(state): State, 236 | payload: Json, 237 | ) -> Result> { 238 | println!(">> HANDLER: verify_email_request_handler called"); 239 | 240 | if !Validation::email(&payload.email) { 241 | return Err(Error::InvalidPayload { 242 | message: "Invalid Email".to_string(), 243 | }); 244 | } 245 | 246 | match User::verify_email_request(&State(&state).mongo_client, &payload.email).await { 247 | Ok(_) => { 248 | return Ok(Json(UserEmailResponse { 249 | message: "Verification email sent".to_string(), 250 | email: payload.email.to_owned(), 251 | })); 252 | } 253 | Err(e) => return Err(e), 254 | } 255 | } 256 | 257 | #[debug_handler] 258 | pub async fn verify_email_handler( 259 | State(state): State, 260 | Path(id): Path, 261 | ) -> Result> { 262 | println!(">> HANDLER: verify_email_handler called"); 263 | 264 | match User::verify_email(&State(&state).mongo_client, &id).await { 265 | Ok(req_id) => { 266 | return Ok(Json(EmailVerificationResponse { 267 | message: "Email verified successfully".to_string(), 268 | req_id: req_id.to_owned(), 269 | })); 270 | } 271 | Err(e) => return Err(e), 272 | } 273 | } 274 | 275 | pub async fn delete_user_handler( 276 | State(state): State, 277 | payload: Json, 278 | ) -> Result> { 279 | println!(">> HANDLER: delete_user_handler called"); 280 | 281 | if !Validation::email(&payload.email) { 282 | return Err(Error::InvalidPayload { 283 | message: "Invalid Email".to_string(), 284 | }); 285 | } 286 | 287 | match User::delete(&State(&state).mongo_client, &payload.email).await { 288 | Ok(uid) => { 289 | match Session::delete_all(&State(&state).mongo_client, &uid).await { 290 | Ok(_) => {} 291 | Err(e) => return Err(e), 292 | } 293 | return Ok(Json(UserEmailResponse { 294 | message: "User deleted".to_string(), 295 | email: payload.email.to_owned(), 296 | })); 297 | } 298 | Err(e) => return Err(e), 299 | } 300 | } 301 | 302 | #[debug_handler] 303 | pub async fn block_user_handler( 304 | Path(id): Path, 305 | State(state): State, 306 | ) -> Result> { 307 | println!(">> HANDLER: block_request called"); 308 | 309 | match User::block(&State(&state).mongo_client, &id).await { 310 | Ok(_) => { 311 | return Ok(Json(BlockUserResponse { 312 | message: "User blocked".to_string(), 313 | req_id: id.to_owned(), 314 | })); 315 | } 316 | Err(e) => return Err(e), 317 | } 318 | } 319 | 320 | #[debug_handler] 321 | pub async fn show_verification_page_email(Path(id): Path) -> impl IntoResponse { 322 | Html(format!( 323 | r#" 324 | 325 | 326 | 327 | 328 | 329 | Verify Email 330 | 338 | 339 | 340 | 343 |
344 |
345 |

Verifying...

346 |

347 |
348 |
349 | 371 | 372 | 373 | "#, 374 | id = id, 375 | api_key = dotenv::var("X_API_KEY").expect("X_API_KEY is not set") 376 | )) 377 | } 378 | 379 | 380 | #[debug_handler] 381 | pub async fn show_block_user_page(Path(id): Path) -> impl IntoResponse { 382 | Html(format!( 383 | r#" 384 | 385 | 386 | 387 | 388 | 389 | Block Account 390 | 398 | 399 | 400 | 403 |
404 |
405 |

Authenticating...

406 |

407 |
408 |
409 | 431 | 432 | 433 | "#, 434 | id = id, 435 | api_key = dotenv::var("X_API_KEY").expect("X_API_KEY is not set") 436 | )) 437 | } 438 | 439 | -------------------------------------------------------------------------------- /src/core/session.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::{Error, Result}, 3 | models::session_model::SessionResponse, 4 | traits::{decryption::Decrypt, encryption::Encrypt}, 5 | utils::{ 6 | email_utils::Email, encryption_utils::Encryption, session_utils::{IDToken, RefreshToken} 7 | }, 8 | }; 9 | use bson::{doc, DateTime}; 10 | use woothee::parser::Parser; 11 | use futures::StreamExt; 12 | use mongodb::{Client, Collection}; 13 | use serde::{Deserialize, Serialize}; 14 | use uuid::Uuid; 15 | 16 | use super::{dek::Dek, user::User}; 17 | 18 | #[derive(Debug, Clone, Serialize, Deserialize)] 19 | pub struct Session { 20 | pub uid: String, 21 | pub session_id: String, 22 | pub email: String, 23 | pub id_token: String, 24 | pub refresh_token: String, 25 | pub user_agent: String, 26 | pub os: String, 27 | pub os_version: String, 28 | pub vendor: String, 29 | pub device: String, 30 | pub browser: String, 31 | pub browser_version: String, 32 | pub is_revoked: bool, 33 | pub created_at: DateTime, 34 | pub updated_at: DateTime, 35 | } 36 | 37 | impl Session { 38 | pub fn new(user: &User, user_agent: &str) -> Self { 39 | let id_token = match IDToken::new(user).sign() { 40 | Ok(token) => token, 41 | Err(_) => "".to_string(), 42 | }; 43 | 44 | let refresh_token = match RefreshToken::new(&user.uid).sign() { 45 | Ok(token) => token, 46 | Err(_) => "".to_string(), 47 | }; 48 | 49 | let parser = Parser::new(); 50 | 51 | let user_agent_data = parser.parse(user_agent); 52 | 53 | let os = user_agent_data.as_ref().map_or_else(String::new, |result| result.os.to_string()); 54 | 55 | let os_version = user_agent_data 56 | .as_ref() 57 | .map_or_else(String::new, |result| result.os_version.to_string()); 58 | 59 | let vendor = user_agent_data 60 | .as_ref() 61 | .map_or_else(String::new, |result| result.vendor.to_string()); 62 | 63 | let device = user_agent_data 64 | .as_ref() 65 | .map_or_else(String::new, |result| result.category.to_string()); 66 | 67 | let browser = user_agent_data 68 | .as_ref() 69 | .map_or_else(String::new, |result| result.name.to_string()); 70 | 71 | let browser_version = user_agent_data 72 | .as_ref() 73 | .map_or_else(String::new, |result| result.version.to_string()); 74 | 75 | 76 | Self { 77 | uid: user.uid.to_string(), 78 | session_id: Uuid::new_v4().to_string(), 79 | email: user.email.to_string(), 80 | id_token, 81 | refresh_token, 82 | user_agent: user_agent.to_string(), 83 | os, 84 | os_version, 85 | vendor, 86 | device, 87 | browser, 88 | browser_version, 89 | is_revoked: false, 90 | created_at: DateTime::now(), 91 | updated_at: DateTime::now(), 92 | } 93 | } 94 | 95 | pub async fn encrypt_add(&self, mongo_client: &Client, key: &str) -> Result { 96 | let db = mongo_client.database("auth"); 97 | let collection_session: Collection = db.collection("sessions"); 98 | 99 | let encrypted_session = self.encrypt(key); 100 | 101 | match collection_session.insert_one(encrypted_session, None).await { 102 | Ok(_) => Ok(self.clone()), 103 | Err(e) => Err(Error::ServerError { 104 | message: e.to_string(), 105 | }), 106 | } 107 | } 108 | 109 | pub async fn verify(mongo_client: &Client, id_token: &str) -> Result<(IDToken, bool)> { 110 | let token_data = match IDToken::verify(&id_token) { 111 | Ok(token_verify_result) => { 112 | // check if the session is expired using the boolean 113 | if !token_verify_result.1 { 114 | return Ok(token_verify_result); 115 | } 116 | let db = mongo_client.database("auth"); 117 | let collection_session: Collection = db.collection("sessions"); 118 | 119 | let dek_data = match Dek::get(mongo_client, &token_verify_result.0.uid).await { 120 | Ok(dek) => dek, 121 | Err(e) => return Err(e), 122 | }; 123 | 124 | let encrypted_id = 125 | Encryption::encrypt_data(&token_verify_result.0.uid, &dek_data.dek); 126 | let encrypted_id_token = Encryption::encrypt_data(&id_token, &dek_data.dek); 127 | 128 | let session = match collection_session 129 | .count_documents( 130 | doc! { 131 | "uid": encrypted_id, 132 | "id_token": encrypted_id_token, 133 | "is_revoked": false, 134 | }, 135 | None, 136 | ) 137 | .await 138 | { 139 | Ok(count) => { 140 | if count == 1 { 141 | Ok(()) 142 | } else { 143 | Err(Error::SessionExpired { 144 | message: "Invalid token".to_string(), 145 | }) 146 | } 147 | } 148 | Err(e) => Err(Error::ServerError { 149 | message: e.to_string(), 150 | }), 151 | }; 152 | if session.is_err() { 153 | return Err(Error::InvalidToken { 154 | message: "Invalid token".to_string(), 155 | }); 156 | } else { 157 | Ok(token_verify_result) 158 | } 159 | } 160 | Err(e) => return Err(e), 161 | }; 162 | token_data 163 | } 164 | 165 | pub async fn refresh( 166 | mongo_client: &Client, 167 | uid: &str, 168 | session_id: &str, 169 | id_token: &str, 170 | refresh_token: &str, 171 | user_agent: &str, 172 | ) -> Result<(String, String)> { 173 | // verify refresh token 174 | match RefreshToken::verify(&refresh_token) { 175 | Ok(_) => {} 176 | Err(e) => { 177 | match Self::revoke(&mongo_client, &session_id, &uid).await { 178 | Ok(_) => return Err(e), 179 | Err(err) => return Err(err), 180 | } 181 | }, 182 | } 183 | match Self::verify(&mongo_client, &id_token).await { 184 | Ok(token_verify_result) => { 185 | if !token_verify_result.1 { 186 | let db = mongo_client.database("auth"); 187 | let collection_session: Collection = db.collection("sessions"); 188 | 189 | let dek_data = match Dek::get(mongo_client, &token_verify_result.0.uid).await { 190 | Ok(dek) => dek, 191 | Err(e) => return Err(e), 192 | }; 193 | 194 | let encrypted_uid = Encryption::encrypt_data(&token_verify_result.0.uid, &dek_data.dek); 195 | let encrypted_id_token = Encryption::encrypt_data(&id_token, &dek_data.dek); 196 | let encrypted_refresh_token = 197 | Encryption::encrypt_data(&refresh_token, &dek_data.dek); 198 | let encrypted_session_id = Encryption::encrypt_data(&session_id, &dek_data.dek); 199 | 200 | match collection_session 201 | .find_one( 202 | doc! { 203 | "uid": &encrypted_uid, 204 | "session_id": &encrypted_session_id, 205 | "is_revoked": false, 206 | }, 207 | None, 208 | ) 209 | .await 210 | { 211 | Ok(session) => { 212 | match session { 213 | Some(data) => { 214 | let decrypted_session = data.decrypt(&dek_data.dek); 215 | if decrypted_session.user_agent != user_agent { 216 | let user = User::get_from_email(mongo_client, &decrypted_session.email).await.unwrap(); 217 | Email::new( 218 | &user.name, 219 | &user.email, 220 | &"Unauthorized Login Attempt Detected", 221 | "We have detected an unauthorized login attempt associated with your account. For your security, we have taken action to protect your account. 222 | 223 | If you attempted to log in, please disregard this message. However, if you did not attempt to log in, we recommend taking the following steps: 224 | 225 | Immediately change your password to a strong, unique one. 226 | Review your account activity for any suspicious activity. 227 | If you have any concerns or questions, please don't hesitate to contact our support team. 228 | 229 | Stay safe and secure, 230 | FlexAuth Team").send().await; 231 | return Err(Error::InvalidUserAgent { 232 | message: "User Agent doesn't match with it's Session's User Agent".to_string(), 233 | }) 234 | } 235 | if decrypted_session.id_token == id_token 236 | && decrypted_session.refresh_token == refresh_token 237 | { 238 | // generate a new id token and refresh token 239 | let user = match User::get_from_uid(&mongo_client, &token_verify_result.0.uid).await { 240 | Ok(user) => user, 241 | Err(e) => return Err(e), 242 | }; 243 | let new_id_token = match IDToken::new(&user).sign() { 244 | Ok(token) => token, 245 | Err(_) => "".to_string(), 246 | }; 247 | 248 | let new_refresh_token = match RefreshToken::new(&token_verify_result.0.uid).sign() { 249 | Ok(token) => token, 250 | Err(_) => "".to_string(), 251 | }; 252 | 253 | // encrypt the new tokens 254 | let new_id_token_encrypted = Encryption::encrypt_data(&new_id_token, &dek_data.dek); 255 | let new_refresh_token_encrypted = Encryption::encrypt_data(&new_refresh_token, &dek_data.dek); 256 | 257 | match collection_session 258 | .update_one( 259 | doc! { 260 | "uid": encrypted_uid, 261 | "id_token": encrypted_id_token, 262 | "refresh_token": encrypted_refresh_token, 263 | "is_revoked": false, 264 | }, 265 | doc! { 266 | "$set": { 267 | "id_token": new_id_token_encrypted, 268 | "refresh_token": new_refresh_token_encrypted, 269 | "updated_at": DateTime::now(), 270 | } 271 | }, 272 | None, 273 | ) 274 | .await 275 | { 276 | Ok(_) => return Ok((new_id_token, new_refresh_token)), 277 | Err(e) => return Err(Error::ServerError { 278 | message: e.to_string(), 279 | }), 280 | }; 281 | } else { 282 | match Self::revoke(&mongo_client, &session_id, &uid).await { 283 | Ok(_) => return Err(Error::InvalidToken { 284 | message: "Invalid token".to_string(), 285 | }), 286 | Err(e) => return Err(e), 287 | } 288 | } 289 | } 290 | None => { 291 | return Err(Error::SessionExpired { 292 | message: "Invalid token".to_string(), 293 | }); 294 | } 295 | } 296 | } 297 | Err(e) => return Err(Error::ServerError { 298 | message: e.to_string(), 299 | }), 300 | }; 301 | } else { 302 | return Err(Error::ActiveSessionExists { 303 | message: "Active Session already exists".to_string(), 304 | }); 305 | } 306 | } 307 | Err(e) => { 308 | match Self::revoke(&mongo_client, &session_id, &uid).await { 309 | Ok(_) => return Err(e), 310 | Err(err) => return Err(err), 311 | } 312 | } 313 | }; 314 | } 315 | 316 | pub async fn get_all(mongo_client: &Client) -> Result> { 317 | let db = mongo_client.database("auth"); 318 | let collection_session: Collection = db.collection("sessions"); 319 | 320 | // get all the sessions 321 | let mut cursor = collection_session.find(None, None).await.unwrap(); 322 | 323 | let mut sessions = Vec::new(); 324 | 325 | while let Some(session) = cursor.next().await { 326 | match session { 327 | Ok(data) => { 328 | let dek_data = match Dek::get(mongo_client, &data.uid).await { 329 | Ok(dek) => dek, 330 | Err(e) => return Err(e), 331 | }; 332 | 333 | let decrypted_session = data.decrypt(&dek_data.dek); 334 | 335 | sessions.push(SessionResponse { 336 | uid: decrypted_session.uid, 337 | session_id: decrypted_session.session_id, 338 | email: decrypted_session.email, 339 | user_agent: decrypted_session.user_agent, 340 | os: decrypted_session.os, 341 | os_version: decrypted_session.os_version, 342 | vendor: decrypted_session.vendor, 343 | device: decrypted_session.device, 344 | browser: decrypted_session.browser, 345 | browser_version: decrypted_session.browser_version, 346 | is_revoked: decrypted_session.is_revoked, 347 | created_at: decrypted_session.created_at, 348 | updated_at: decrypted_session.updated_at, 349 | }); 350 | } 351 | Err(_) => { 352 | return Err(Error::ServerError { 353 | message: "Failed to get session".to_string(), 354 | }); 355 | } 356 | } 357 | } 358 | // let collection_dek: Collection = db.collection("deks"); 359 | 360 | // let mut cursor_dek = collection_dek.find(None, None).await.unwrap(); 361 | 362 | // let mut sessions = Vec::new(); 363 | // let kek = env::var("SERVER_KEK").expect("Server Kek must be set."); 364 | 365 | // // iterate over the sessions and decrypt the data 366 | // while let Some(dek) = cursor_dek.next().await { 367 | // let dek_data: Dek = match dek { 368 | // Ok(data) => data.decrypt(&kek), 369 | // Err(_) => { 370 | // return Err(Error::ServerError { 371 | // message: "Failed to get DEK".to_string(), 372 | // }); 373 | // } 374 | // }; 375 | 376 | // // find the session in the sessions collection using the encrypted email to iterate over the sessions 377 | // let cursor_session = collection_session 378 | // .find_one( 379 | // Some(doc! { 380 | // "uid": &dek_data.uid, 381 | // }), 382 | // None, 383 | // ) 384 | // .await 385 | // .unwrap(); 386 | 387 | // match cursor_session { 388 | // Some(session) => { 389 | // let session_data = session.decrypt(&dek_data.dek); 390 | 391 | // sessions.push(SessionResponse { 392 | // uid: session_data.uid, 393 | // session_id: session_data.session_id, 394 | // email: session_data.email, 395 | // user_agent: session_data.user_agent, 396 | // is_revoked: session_data.is_revoked, 397 | // created_at: session_data.created_at, 398 | // updated_at: session_data.updated_at, 399 | // }); 400 | // } 401 | // None => {()} 402 | // } 403 | // } 404 | 405 | // sort the sessions by created_at 406 | sessions.sort_by(|a, b| a.created_at.cmp(&b.created_at)); 407 | Ok(sessions) 408 | } 409 | 410 | pub async fn get_all_from_uid( 411 | mongo_client: &Client, 412 | uid: &str, 413 | ) -> Result> { 414 | let db = mongo_client.database("auth"); 415 | let collection_session: Collection = db.collection("sessions"); 416 | 417 | let dek_data = match Dek::get(mongo_client, uid).await { 418 | Ok(dek) => dek, 419 | Err(e) => return Err(e), 420 | }; 421 | 422 | let mut cursor = collection_session 423 | .find( 424 | doc! { 425 | "uid": uid, 426 | }, 427 | None, 428 | ) 429 | .await 430 | .unwrap(); 431 | 432 | let mut sessions_res: Vec = Vec::new(); 433 | while let Some(session) = cursor.next().await { 434 | match session { 435 | Ok(data) => { 436 | let decrypted_session = data.decrypt(&dek_data.dek); 437 | match IDToken::verify(&decrypted_session.id_token) { 438 | Ok(token) => { 439 | println!("{:?}", token); 440 | sessions_res.push(SessionResponse { 441 | uid: decrypted_session.uid, 442 | session_id: decrypted_session.session_id, 443 | email: decrypted_session.email, 444 | user_agent: decrypted_session.user_agent, 445 | os: decrypted_session.os, 446 | os_version: decrypted_session.os_version, 447 | vendor: decrypted_session.vendor, 448 | device: decrypted_session.device, 449 | browser: decrypted_session.browser, 450 | browser_version: decrypted_session.browser_version, 451 | is_revoked: decrypted_session.is_revoked, 452 | created_at: decrypted_session.created_at, 453 | updated_at: decrypted_session.updated_at, 454 | }); 455 | } 456 | Err(_) => continue, 457 | } 458 | } 459 | Err(e) => { 460 | return Err(Error::ServerError { 461 | message: e.to_string(), 462 | }) 463 | } 464 | } 465 | } 466 | Ok(sessions_res) 467 | } 468 | 469 | pub async fn get_details(mongo_client: &Client, uid: &str, session_id: &str) -> Result { 470 | let db = mongo_client.database("auth"); 471 | let collection_session: Collection = db.collection("sessions"); 472 | 473 | let dek_data = match Dek::get(mongo_client, uid).await { 474 | Ok(dek) => dek, 475 | Err(e) => return Err(e), 476 | }; 477 | 478 | let encrypted_session_id = Encryption::encrypt_data(session_id, &dek_data.dek); 479 | 480 | let session = match collection_session 481 | .find_one(doc! {"uid": &uid, "session_id": encrypted_session_id}, None) 482 | .await 483 | { 484 | Ok(session) => { 485 | match session { 486 | Some(data) => { 487 | let decrypted_session = data.decrypt(&dek_data.dek); 488 | Ok(decrypted_session) 489 | } 490 | None => Err(Error::SessionNotFound { 491 | message: "Session not found".to_string(), 492 | }), 493 | } 494 | } 495 | Err(e) => Err(Error::ServerError { 496 | message: e.to_string(), 497 | }), 498 | }; 499 | 500 | match session { 501 | Ok(data) => { 502 | Ok(SessionResponse { 503 | uid: data.uid, 504 | session_id: data.session_id, 505 | email: data.email, 506 | user_agent: data.user_agent, 507 | os: data.os, 508 | os_version: data.os_version, 509 | vendor: data.vendor, 510 | device: data.device, 511 | browser: data.browser, 512 | browser_version: data.browser_version, 513 | is_revoked: data.is_revoked, 514 | created_at: data.created_at, 515 | updated_at: data.updated_at, 516 | }) 517 | } 518 | Err(e) => Err(e), 519 | } 520 | } 521 | 522 | pub async fn revoke_all(mongo_client: &Client, uid: &str) -> Result<()> { 523 | let db = mongo_client.database("auth"); 524 | let collection_session: Collection = db.collection("sessions"); 525 | 526 | match collection_session 527 | .update_many(doc! {"uid": &uid }, doc! {"$set": {"is_revoked": true}}, None) 528 | .await 529 | { 530 | Ok(_) => Ok(()), 531 | Err(e) => Err(Error::ServerError { 532 | message: e.to_string(), 533 | }), 534 | } 535 | } 536 | 537 | pub async fn revoke(mongo_client: &Client, session_id: &str, uid: &str) -> Result<()> { 538 | let db = mongo_client.database("auth"); 539 | let collection_session: Collection = db.collection("sessions"); 540 | 541 | let dek_data = match Dek::get(mongo_client, uid).await { 542 | Ok(dek) => dek, 543 | Err(e) => return Err(e), 544 | }; 545 | 546 | let encrypted_session_id = Encryption::encrypt_data(session_id, &dek_data.dek); 547 | 548 | match collection_session 549 | .update_one( 550 | doc! {"session_id": encrypted_session_id}, 551 | doc! {"$set": {"is_revoked": true}}, 552 | None, 553 | ) 554 | .await 555 | { 556 | Ok(_) => Ok(()), 557 | Err(e) => Err(Error::ServerError { 558 | message: e.to_string(), 559 | }), 560 | } 561 | } 562 | 563 | pub async fn delete(mongo_client: &Client, session_id: &str, uid: &str) -> Result<()> { 564 | let db = mongo_client.database("auth"); 565 | let collection_session: Collection = db.collection("sessions"); 566 | 567 | let dek_data = match Dek::get(mongo_client, uid).await { 568 | Ok(dek) => dek, 569 | Err(e) => return Err(e), 570 | }; 571 | 572 | let encrypted_session_id = Encryption::encrypt_data(session_id, &dek_data.dek); 573 | 574 | match collection_session 575 | .delete_one( 576 | doc! { "session_id": encrypted_session_id }, 577 | None, 578 | ) 579 | .await 580 | { 581 | Ok(_) => Ok(()), 582 | Err(e) => Err(Error::ServerError { 583 | message: e.to_string(), 584 | }), 585 | } 586 | } 587 | 588 | pub async fn delete_all(mongo_client: &Client, uid: &str) -> Result<()> { 589 | let db = mongo_client.database("auth"); 590 | let collection_session: Collection = db.collection("sessions"); 591 | 592 | match collection_session 593 | .delete_many(doc! {"uid": &uid }, None) 594 | .await 595 | { 596 | Ok(_) => Ok(()), 597 | Err(e) => Err(Error::ServerError { 598 | message: e.to_string(), 599 | }), 600 | } 601 | } 602 | } 603 | --------------------------------------------------------------------------------