From 0ba58d90e34145ca2c91dd0d1de23a1cfefa8486 Mon Sep 17 00:00:00 2001 From: Miroito Date: Sun, 4 Jun 2023 21:09:54 +0200 Subject: [PATCH] feat: Set up user backend --- server/Cargo.lock | 251 +++++++++++++++++- server/Cargo.toml | 5 + .../src/m20230604_113236_user_table.rs | 4 +- server/src/env.rs | 3 + server/src/error.rs | 6 + server/src/main.rs | 16 +- server/src/repo/mod.rs | 1 + server/src/repo/user.rs | 23 ++ server/src/route/mod.rs | 1 + server/src/route/user.rs | 133 ++++++++++ 10 files changed, 435 insertions(+), 8 deletions(-) create mode 100644 server/src/repo/user.rs create mode 100644 server/src/route/user.rs diff --git a/server/Cargo.lock b/server/Cargo.lock index 2e5a50d..20999b6 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -14,6 +14,41 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.6" @@ -51,6 +86,12 @@ dependencies = [ "libc", ] +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + [[package]] name = "arrayvec" version = "0.7.2" @@ -292,6 +333,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.0" @@ -321,6 +368,17 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2b_simd" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c2f0dc9a68c6317d884f97cc36cf5a3d20ba14ce404227df55e1af708ab04bc" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq 0.2.5", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -461,6 +519,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "3.2.24" @@ -528,6 +596,36 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "constant_time_eq" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "aes-gcm", + "base64 0.21.0", + "hkdf", + "hmac", + "percent-encoding", + "rand", + "sha2", + "subtle", + "time 0.3.20", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -598,6 +696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -611,6 +710,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "cxx" version = "1.0.94" @@ -674,12 +782,13 @@ checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690" [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -994,6 +1103,16 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gloo-timers" version = "0.2.6" @@ -1091,6 +1210,24 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "0.2.9" @@ -1225,6 +1362,15 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -1275,6 +1421,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.0", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -1538,6 +1698,12 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.52" @@ -1677,6 +1843,15 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "pem-rfc7468" version = "0.3.1" @@ -1768,6 +1943,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "polyval" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "pom" version = "3.2.0" @@ -1950,7 +2137,7 @@ version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" dependencies = [ - "base64", + "base64 0.21.0", "bytes", "encoding_rs", "futures-core", @@ -2046,6 +2233,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-argon2" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50162d19404029c1ceca6f6980fe40d45c8b369f6f44446fa14bb39573b5bb9" +dependencies = [ + "base64 0.13.1", + "blake2b_simd", + "constant_time_eq 0.1.5", + "crossbeam-utils", +] + [[package]] name = "rust_decimal" version = "1.29.1" @@ -2105,7 +2304,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64", + "base64 0.21.0", ] [[package]] @@ -2418,15 +2617,19 @@ dependencies = [ "axum", "bytes", "chrono", + "cookie", "dotenvy", "envy", "futures", "hyper", + "jsonwebtoken", "lazy_static", "log", "lopdf", "migration", + "rand", "reqwest", + "rust-argon2", "sea-orm", "serde", "serde_json", @@ -2434,6 +2637,7 @@ dependencies = [ "thiserror", "tokio", "tower", + "tower-cookies", "tower-http", "tracing", "tracing-subscriber", @@ -2500,6 +2704,18 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.20", +] + [[package]] name = "slab" version = "0.4.8" @@ -3008,6 +3224,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-cookies" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40f38d941a2ffd8402b36e02ae407637a9caceb693aaf2edc910437db0f36984" +dependencies = [ + "async-trait", + "axum-core", + "cookie", + "futures-util", + "http", + "parking_lot 0.12.1", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.4.0" @@ -3152,6 +3385,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" diff --git a/server/Cargo.toml b/server/Cargo.toml index d44f8c3..31473ee 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -17,6 +17,7 @@ reqwest = { version = "0.11", features = ["json", "rustls-tls"] } axum = "0.6.12" hyper = { version = "0.14.25", features = ["full"] } tower = "0.4" +tower-cookies = "0.9" sea-orm = { version = "0.11.0", features = [ "runtime-tokio-rustls", "macros", @@ -33,3 +34,7 @@ slug = "0.1.4" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tower-http = { version = "0.4", features = ["trace", "cors"] } tracing = "0.1" +rust-argon2 = "1" +rand = "0.8" +jsonwebtoken = "8" +cookie = { version = "0.17", features = [ "secure" ] } diff --git a/server/migration/src/m20230604_113236_user_table.rs b/server/migration/src/m20230604_113236_user_table.rs index 03b5878..b4fddad 100644 --- a/server/migration/src/m20230604_113236_user_table.rs +++ b/server/migration/src/m20230604_113236_user_table.rs @@ -18,8 +18,8 @@ impl MigrationTrait for Migration { .auto_increment() .primary_key(), ) - .col(ColumnDef::new(User::Email).string().not_null()) - .col(ColumnDef::new(User::Name).string().not_null()) + .col(ColumnDef::new(User::Email).string().not_null().unique_key()) + .col(ColumnDef::new(User::Name).string().not_null().unique_key()) .col(ColumnDef::new(User::Password).string().not_null()) .to_owned(), ) diff --git a/server/src/env.rs b/server/src/env.rs index 98fda18..48f3730 100644 --- a/server/src/env.rs +++ b/server/src/env.rs @@ -8,6 +8,7 @@ pub struct Env { pub host: String, #[serde(default = "port_default")] pub port: String, + pub api_url: String, pub mysql_user: String, pub mysql_password: String, pub mysql_host: String, @@ -107,6 +108,7 @@ impl Env { #[derive(Debug)] pub struct Config { pub server_address: SocketAddr, + pub server_domain: String, pub database_url: String, pub max_connections: u32, pub min_connections: u32, @@ -134,6 +136,7 @@ impl Config { let mut config = Config { server_address, + server_domain: env.api_url, database_url, max_connections: env.max_connections, min_connections: env.min_connections, diff --git a/server/src/error.rs b/server/src/error.rs index cc5d1bc..336515d 100644 --- a/server/src/error.rs +++ b/server/src/error.rs @@ -12,6 +12,9 @@ pub enum AppError { DbErr(DbErr), InProcessTransaction(InProcessTransactionError), NotFound(String), + InternalServerError(String), + Unauthorized, + Conflict(String), } impl From for AppError { @@ -38,6 +41,9 @@ impl IntoResponse for AppError { format!("Error in the in process transaction repo: {}", e), ), AppError::NotFound(e) => (StatusCode::NOT_FOUND, format!("Not found error: {}", e)), + AppError::InternalServerError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e), + AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".to_string()), + AppError::Conflict(e) => (StatusCode::CONFLICT, e), }; let body = Json(json!({ diff --git a/server/src/main.rs b/server/src/main.rs index cf6f622..07c1231 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -6,15 +6,18 @@ extern crate lazy_static; #[macro_use] extern crate log; +extern crate argon2; + // External crates use axum::{ extract::MatchedPath, http::{HeaderValue, Method, Request, StatusCode}, response::Response, - routing::get, + routing::{get, post}, Router, }; use sea_orm::DatabaseConnection; +use tower_cookies::CookieManagerLayer; use std::time::Duration; use tokio::signal; use tower_http::{classify::ServerErrorsFailureClass, cors::CorsLayer, trace::TraceLayer}; @@ -33,7 +36,7 @@ mod model; mod repo; mod route; mod task; -use crate::task::run_tasks; +use crate::{route::user, task::run_tasks}; // Module imports use env::Config; @@ -96,6 +99,14 @@ pub async fn main() -> Result<(), Box> { "/in_process_transaction/retry_all", get(in_process_transaction::retry_all), ) + .route( + "/user/login", + post(user::login), + ) + .route( + "/user/register", + post(user::register), + ) .with_state(shared_state.clone()) .fallback(fallback) .layer( @@ -129,6 +140,7 @@ pub async fn main() -> Result<(), Box> { }, ), ) + .layer(CookieManagerLayer::new()) .layer( CorsLayer::new() .allow_origin("*".parse::().unwrap()) diff --git a/server/src/repo/mod.rs b/server/src/repo/mod.rs index d805fca..747d33e 100644 --- a/server/src/repo/mod.rs +++ b/server/src/repo/mod.rs @@ -1,3 +1,4 @@ pub mod company; pub mod in_process_transaction; pub mod transaction; +pub mod user; diff --git a/server/src/repo/user.rs b/server/src/repo/user.rs new file mode 100644 index 0000000..52e1fb2 --- /dev/null +++ b/server/src/repo/user.rs @@ -0,0 +1,23 @@ +use crate::model::user::ActiveModel; +use sea_orm::{ActiveModelTrait, ConnectionTrait, DbErr, DeriveIntoActiveModel, IntoActiveModel}; +use serde::{Deserialize, Serialize}; + +use crate::model; + +#[derive(Debug, PartialEq, Clone, DeriveIntoActiveModel, Serialize, Deserialize)] +pub struct NewUser { + pub email: String, + pub name: String, + pub password: String, +} + +impl NewUser { + pub async fn create(&self, db: &C) -> Result + where + C: ConnectionTrait, + { + let res = self.clone().into_active_model().insert(db).await?; + + Ok(res) + } +} diff --git a/server/src/route/mod.rs b/server/src/route/mod.rs index 28de2ea..a358233 100644 --- a/server/src/route/mod.rs +++ b/server/src/route/mod.rs @@ -5,6 +5,7 @@ use serde::{de, Deserialize, Deserializer}; pub mod company; pub mod in_process_transaction; pub mod transaction; +pub mod user; /// Struct to deserialize paginated routes query parameters #[derive(Deserialize)] diff --git a/server/src/route/user.rs b/server/src/route/user.rs new file mode 100644 index 0000000..ff3986e --- /dev/null +++ b/server/src/route/user.rs @@ -0,0 +1,133 @@ +use axum::{extract::State, Json}; +use cookie::{time::Duration, Cookie, SameSite}; +use jsonwebtoken::{encode, EncodingKey, Header}; +use rand::RngCore; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; +use serde::{Deserialize, Serialize}; +use tower_cookies::Cookies; + +use crate::{error::AppError, model, repo::user::NewUser, AppState, CONFIG}; + +#[derive(Deserialize)] +pub struct UserLoginBody { + pub name: String, + pub password: String, +} + +#[derive(Serialize, Deserialize)] +pub struct JWTClaim { + pub username: String, +} + +pub async fn login( + cookies: Cookies, + State(state): State, + Json(payload): Json, +) -> Result<(), AppError> { + let db = &state.db; + let user_opt = model::user::Entity::find() + .filter(model::user::Column::Name.eq(payload.name)) + .one(db) + .await?; + + if user_opt.is_none() { + // To prevent timing attacks, we use the same verify function on a known password + argon2::verify_encoded("$argon2i$v=19$m=4096,t=3,p=1$CXr/AgSDawghR+GmOhM0wQ$4k2TCyoqkh/YaK9mh6uEa0eRZ/CIx3bfzJs5UnCcKjw", b"1234").unwrap(); + return Err(AppError::NotFound("User not found".to_string())); + } + + let user = user_opt.unwrap(); + + let valid = + argon2::verify_encoded(&user.password, payload.password.as_bytes()).map_err(|e| { + error!("Error verifying the password for user {}: {}", user.name, e); + AppError::InternalServerError("There was an error verifying authentication".to_string()) + })?; + + if !valid { + return Err(AppError::Unauthorized); + } + + // Generate a JWT and store it as a same site cookie + let claim = JWTClaim { + username: user.name, + }; + + let token_str = encode( + &Header::default(), + &claim, + &EncodingKey::from_secret(b"some-secret"), + ) + .map_err(|e| { + error!("Failed to encode a JWT: {}", e); + AppError::InternalServerError("There was an error verifying authentication".to_string()) + })?; + + let cookie = Cookie::build("auth", token_str) + .domain(CONFIG.server_domain.clone()) + .path("/") + .same_site(SameSite::Strict) + .secure(true) + .http_only(true) + .max_age(Duration::days(4)) + .finish(); + + cookies.add(cookie); + + Ok(()) +} + +#[derive(Deserialize)] +pub struct UserRegisterBody { + pub name: String, + pub email: String, + pub password: String, +} + +pub async fn register( + State(state): State, + Json(payload): Json, +) -> Result<(), AppError> { + let db = &state.db; + let user_opt = model::user::Entity::find() + .filter( + model::user::Column::Name + .eq(&payload.name) + .or(model::user::Column::Email.eq(&payload.email)), + ) + .one(db) + .await?; + + if user_opt.is_some() { + return Err(AppError::Conflict( + "The username or email is already in use".to_string(), + )); + } + + let salt = generate_salt(); + let pass_hash = + argon2::hash_encoded(payload.password.as_ref(), &salt, &argon2::Config::default()) + .map_err(|e| { + error!("Failed to hash a password: {}", e); + AppError::InternalServerError( + "There was an error in the registration process".to_string(), + ) + })?; + + let new_user = NewUser { + email: payload.email, + name: payload.name, + password: pass_hash, + }; + + new_user.create(db).await?; + + Ok(()) +} + +fn generate_salt() -> Vec { + let mut salt = [0u8; 16]; // 16 bytes salt length (adjust as needed) + rand::thread_rng().fill_bytes(&mut salt); + + salt.into() +}