diff --git a/Cargo.lock b/Cargo.lock index 17710449..ea2c4a3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,7 +30,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash 0.8.3", - "base64", + "base64 0.21.2", "bitflags", "brotli", "bytes", @@ -58,6 +58,22 @@ dependencies = [ "zstd", ] +[[package]] +name = "actix-identity" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1224c9f9593dc27c9077b233ce04adedc1d7febcfc35ee9f53ea3c24df180bec" +dependencies = [ + "actix-service", + "actix-session", + "actix-utils", + "actix-web", + "anyhow", + "futures-core", + "serde", + "tracing", +] + [[package]] name = "actix-macros" version = "0.2.3" @@ -120,6 +136,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-session" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da8b818ae1f11049a4d218975345fe8e56ce5a5f92c11f972abcff5ff80e87" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "anyhow", + "async-trait", + "derive_more", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "actix-utils" version = "3.0.1" @@ -189,6 +222,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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +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" @@ -408,6 +476,17 @@ version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" +[[package]] +name = "async-trait" +version = "0.1.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2d0f03b3640e3a630367e40c468cb7f309529c708ed1d88597047b0e7c6ef7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "atoi" version = "1.0.0" @@ -429,6 +508,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "base64" version = "0.21.2" @@ -493,6 +578,8 @@ dependencies = [ name = "brass" version = "0.1.0" dependencies = [ + "actix-identity", + "actix-session", "actix-web", "anyhow", "argon2", @@ -565,6 +652,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[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 = "concurrent-queue" version = "2.2.0" @@ -586,7 +683,14 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", "percent-encoding", + "rand", + "sha2", + "subtle", "time", "version_check", ] @@ -650,9 +754,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -894,6 +1008,16 @@ dependencies = [ "wasi", ] +[[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" @@ -979,6 +1103,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" @@ -1031,6 +1173,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" @@ -1249,6 +1400,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "parking" version = "2.1.0" @@ -1380,6 +1537,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1525,7 +1694,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64", + "base64 0.21.2", ] [[package]] @@ -1975,6 +2144,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/Cargo.toml b/Cargo.toml index 4426935f..38213a43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ sqlx = { version = "0.6", features = [ "runtime-async-std-rustls", "sqlite" ] } actix-web = { version = "4" } askama = "0.12.0" serde = { version = "1.0.164", features = [ "derive"]} -argon2 = "0.5.0" +argon2 = { version = "0.5.0", features = [ "std"]} anyhow = "1.0.71" dotenv = "0.15.0" +actix-session = { version = "0.7.2", features = ["cookie-session"] } +actix-identity = "0.5.2" diff --git a/migrations/20230609121618_initial.sql b/migrations/20230609121618_initial.sql index 2dadac14..08923d9a 100644 --- a/migrations/20230609121618_initial.sql +++ b/migrations/20230609121618_initial.sql @@ -9,9 +9,10 @@ INSERT OR REPLACE INTO roles(id, definition) values(2, "Administrator"); CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - password TEXT NOT NULL, - role INTEGER NOT NULL, - FOREIGN KEY(role) REFERENCES roles(roleId) + id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + password_hash TEXT NOT NULL, + salt TEXT NOT NULL, + role_id INTEGER NOT NULL, + FOREIGN KEY(role_id) REFERENCES roles(id) ); diff --git a/src/auth/routes.rs b/src/auth/routes.rs deleted file mode 100644 index 2d1f731b..00000000 --- a/src/auth/routes.rs +++ /dev/null @@ -1,74 +0,0 @@ -use actix_web::{Responder, web, HttpResponse}; -use askama::Template; -use serde::Deserialize; -use sqlx::sqlite::SqlitePool; - -use crate::auth::utils::hash_plain_password; -use crate::models::Role; - -pub fn init(cfg: &mut web::ServiceConfig) { - cfg.service(get_login); - cfg.service(post_login); - cfg.service(get_register); - cfg.service(post_register); -} - -#[derive(Deserialize)] -struct LoginForm { - name: String, - password: String -} - -#[derive(Template)] -#[template(path = "login.html")] -struct LoginTemplate { -} - -#[derive(Template)] -#[template(path = "register.html")] -struct RegisterTemplate { - roles: Vec -} - -#[actix_web::get("/login")] -async fn get_login() -> impl Responder { - let bla = LoginTemplate {}; - - HttpResponse::Ok() - .body(bla.render().unwrap()) -} - -#[actix_web::post("/login")] -async fn post_login(web::Form(form): web::Form) -> impl Responder { - println!("{} - {}", form.name, form.password); - - let hash = hash_plain_password(&form.password); - - println!("{hash}"); - - "dfgdg" -} - -#[actix_web::get("/register")] -async fn get_register(pool: web::Data) -> impl Responder { - let roles = match Role::GetAll(pool.get_ref()).await { - Ok(value) => value, - Err(error) => return HttpResponse::InternalServerError().body(error.to_string()) - }; - - let bla = RegisterTemplate { roles }; - - HttpResponse::Ok() - .body(bla.render().unwrap()) -} - -#[actix_web::post("/register")] -async fn post_register(web::Form(form): web::Form) -> impl Responder { - println!("{} - {}", form.name, form.password); - - let hash = hash_plain_password(&form.password); - - println!("{hash}"); - - "dfgdg" -} diff --git a/src/auth/routes/get_login.rs b/src/auth/routes/get_login.rs new file mode 100644 index 00000000..84fb19a6 --- /dev/null +++ b/src/auth/routes/get_login.rs @@ -0,0 +1,20 @@ +use actix_identity::Identity; +use actix_web::{Responder, HttpResponse, http::header::LOCATION}; +use askama::Template; + +#[derive(Template)] +#[template(path = "login.html")] +struct LoginTemplate {} + +#[actix_web::get("/login")] +async fn route(user: Option) -> impl Responder { + if let Some(_) = user { + return HttpResponse::PermanentRedirect() + .insert_header((LOCATION, "/")) + .finish(); + } else { + let bla = LoginTemplate {}; + + return HttpResponse::Ok().body(bla.render().unwrap()); + } +} diff --git a/src/auth/routes/get_register.rs b/src/auth/routes/get_register.rs new file mode 100644 index 00000000..b447f8b1 --- /dev/null +++ b/src/auth/routes/get_register.rs @@ -0,0 +1,23 @@ +use actix_web::{web, HttpResponse, Responder}; +use askama::Template; +use sqlx::SqlitePool; + +use crate::models::Role; + +#[derive(Template)] +#[template(path = "register.html")] +struct RegisterTemplate { + roles: Vec, +} + +#[actix_web::get("/register")] +async fn route(pool: web::Data) -> impl Responder { + let roles = match Role::GetAll(pool.get_ref()).await { + Ok(value) => value, + Err(error) => return HttpResponse::InternalServerError().body(error.to_string()), + }; + + let bla = RegisterTemplate { roles }; + + HttpResponse::Ok().body(bla.render().unwrap()) +} diff --git a/src/auth/routes/mod.rs b/src/auth/routes/mod.rs new file mode 100644 index 00000000..30e56da3 --- /dev/null +++ b/src/auth/routes/mod.rs @@ -0,0 +1,13 @@ +mod get_login; +mod get_register; +mod post_login; +mod post_register; + +use actix_web::web; + +pub fn init(cfg: &mut web::ServiceConfig) { + cfg.service(self::get_login::route); + cfg.service(self::post_login::route); + cfg.service(self::get_register::route); + cfg.service(self::post_register::route); +} diff --git a/src/auth/routes/post_login.rs b/src/auth/routes/post_login.rs new file mode 100644 index 00000000..4dd723e8 --- /dev/null +++ b/src/auth/routes/post_login.rs @@ -0,0 +1,34 @@ +use actix_identity::Identity; +use actix_web::{web, HttpMessage, HttpRequest, HttpResponse, Responder}; +use serde::Deserialize; +use sqlx::SqlitePool; + +use crate::{auth::utils::hash_plain_password_with_salt, models::user::User}; + +#[derive(Deserialize)] +struct LoginForm { + name: String, + password: String, +} + +#[actix_web::post("/login")] +async fn route( + web::Form(form): web::Form, + request: HttpRequest, + pool: web::Data, +) -> impl Responder { + match User::read_by_name(pool.get_ref(), &form.name).await { + Some(potential_user) => { + let hash = hash_plain_password_with_salt(&form.password, &potential_user.salt).unwrap(); + + if hash == potential_user.password_hash { + Identity::login(&request.extensions(), potential_user.id.to_string()); + + return HttpResponse::Ok().body("Angemeldet!"); + } else { + return HttpResponse::Unauthorized().body("Nutzername oder Passwort falsch."); + } + } + None => return HttpResponse::Unauthorized().body("Nutzername oder Passwort falsch."), + } +} diff --git a/src/auth/routes/post_register.rs b/src/auth/routes/post_register.rs new file mode 100644 index 00000000..6ba974ae --- /dev/null +++ b/src/auth/routes/post_register.rs @@ -0,0 +1,31 @@ +use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; +use serde::Deserialize; +use sqlx::SqlitePool; + +use crate::{auth::utils::generate_salt_and_hash_plain_password, models::user::User}; + +#[derive(Deserialize)] +struct RegisterForm { + name: String, + password: String, + role: i64, +} + +#[actix_web::post("/register")] +async fn route( + web::Form(form): web::Form, + pool: web::Data, +) -> impl Responder { + let (hash, salt) = generate_salt_and_hash_plain_password(&form.password).unwrap(); + + let result = User::create(pool.get_ref(), form.name, hash, salt, form.role).await; + + match result { + Ok(_) => { + return HttpResponse::PermanentRedirect() + .insert_header((LOCATION, "/")) + .finish() + } + Err(err) => return HttpResponse::BadRequest().body(err.to_string()), + } +} diff --git a/src/auth/utils.rs b/src/auth/utils.rs index 02e05aaa..e50a0cb4 100644 --- a/src/auth/utils.rs +++ b/src/auth/utils.rs @@ -1,20 +1,25 @@ use argon2::{ - password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, SaltString}, + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, Argon2, }; -pub fn hash_plain_password(plain: &str) -> String { +pub fn generate_salt_and_hash_plain_password(plain: &str) -> anyhow::Result<(String, String)> { let salt = SaltString::generate(&mut OsRng); // Argon2 with default params (Argon2id v19) - Argon2::default() - .hash_password(plain.as_bytes(), &salt) - .unwrap() - .to_string() + let hash = Argon2::default() + .hash_password(plain.as_bytes(), &salt)? + .to_string(); - // Verify password against PHC string. - // - // NOTE: hash params from `parsed_hash` are used instead of what is configured in the - // `Argon2` instance. - // PasswordHash::new(&password_hash).unwrap().to_string() + Ok((hash, salt.to_string())) +} + +pub fn hash_plain_password_with_salt(plain: &str, salt_string: &str) -> anyhow::Result { + let salt = SaltString::from_b64(salt_string)?; + + let hash = Argon2::default() + .hash_password(plain.as_bytes(), &salt)? + .to_string(); + + Ok(hash) } diff --git a/src/calendar/mod.rs b/src/calendar/mod.rs new file mode 100644 index 00000000..dee9bfb0 --- /dev/null +++ b/src/calendar/mod.rs @@ -0,0 +1,3 @@ +pub mod routes; + +pub use routes::init; diff --git a/src/calendar/routes.rs b/src/calendar/routes.rs new file mode 100644 index 00000000..d093c779 --- /dev/null +++ b/src/calendar/routes.rs @@ -0,0 +1,12 @@ +use actix_identity::Identity; +use actix_web::{Responder, web, HttpResponse}; + +pub fn init(cfg: &mut web::ServiceConfig) { + cfg.service(get_login); +} + +#[actix_web::get("/")] +async fn get_login(user: Identity) -> impl Responder { + + HttpResponse::Ok().body("Hierfür muss man angemeldet sein!") +} diff --git a/src/main.rs b/src/main.rs index d6d9db0c..694b533c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,34 @@ use std::env; -use actix_web::{App, HttpServer, web}; -use sqlx::sqlite::SqlitePool; +use actix_identity::IdentityMiddleware; +use actix_session::{storage::CookieSessionStore, SessionMiddleware}; +use actix_web::cookie::Key; +use actix_web::{web, App, HttpServer}; use dotenv::dotenv; +use sqlx::sqlite::SqlitePool; mod auth; +mod calendar; mod models; #[actix_web::main] async fn main() -> anyhow::Result<()> { dotenv()?; let pool = SqlitePool::connect(&env::var("DATABASE_URL")?).await?; + let secret_key = Key::generate(); + + println!("Starting server on http://localhost:8080."); HttpServer::new(move || { App::new() - .app_data(web::Data::new(pool.clone())) - .configure(auth::init) + .app_data(web::Data::new(pool.clone())) + .configure(auth::init) + .configure(calendar::init) + .wrap(IdentityMiddleware::default()) + .wrap(SessionMiddleware::new( + CookieSessionStore::default(), + secret_key.clone(), + )) }) .bind(("127.0.0.1", 8080))? .run() diff --git a/src/models/mod.rs b/src/models/mod.rs index 81e0b782..a33b4d37 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,5 @@ +pub mod user; + use sqlx::sqlite::SqlitePool; pub struct Role { diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 00000000..d22a4144 --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,81 @@ +use sqlx::sqlite::SqlitePool; + +pub struct User { + pub id: i64, + pub name: String, + pub password_hash: String, + pub salt: String, + pub role_id: i64, +} + +impl User { + pub async fn create( + pool: &SqlitePool, + name: String, + password_hash: String, + salt: String, + role_id: i64, + ) -> anyhow::Result { + let created = sqlx::query!( + r#" + INSERT INTO users (name, password_hash, salt, role_id) + VALUES (?1, ?2, ?3, ?4); + "#, + name, + password_hash, + salt, + role_id + ) + .execute(pool) + .await; + + match created { + Ok(result) => Ok(result.last_insert_rowid()), + Err(err) => Err(err.into()), + } + } + + pub async fn read_by_id(pool: &SqlitePool, id: i64) -> Option { + let record = sqlx::query_as!( + User, + r#" + SELECT * + FROM users + WHERE id = ?1; + "#, + id, + ) + .fetch_one(pool) + .await; + + match record { + Ok(record) => Some(record), + Err(err) => { + println!("User.read({id}): {err}"); + None + } + } + } + + pub async fn read_by_name(pool: &SqlitePool, name: &str) -> Option { + let record = sqlx::query_as!( + User, + r#" + SELECT * + FROM users + WHERE name = ?1; + "#, + name + ) + .fetch_one(pool) + .await; + + match record { + Ok(record) => Some(record), + Err(err) => { + println!("User.read({name}): {err}"); + None + } + } + } +}