feat: implement basic authetication session

This commit is contained in:
Max Hohlfeld 2023-08-11 14:19:15 +02:00
parent 5807c00b2f
commit 36f4da1457
15 changed files with 442 additions and 97 deletions

183
Cargo.lock generated
View File

@ -30,7 +30,7 @@ dependencies = [
"actix-service", "actix-service",
"actix-utils", "actix-utils",
"ahash 0.8.3", "ahash 0.8.3",
"base64", "base64 0.21.2",
"bitflags", "bitflags",
"brotli", "brotli",
"bytes", "bytes",
@ -58,6 +58,22 @@ dependencies = [
"zstd", "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]] [[package]]
name = "actix-macros" name = "actix-macros"
version = "0.2.3" version = "0.2.3"
@ -120,6 +136,23 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "actix-utils" name = "actix-utils"
version = "3.0.1" version = "3.0.1"
@ -189,6 +222,41 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 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]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.6" version = "0.7.6"
@ -408,6 +476,17 @@ version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" 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]] [[package]]
name = "atoi" name = "atoi"
version = "1.0.0" version = "1.0.0"
@ -429,6 +508,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.2" version = "0.21.2"
@ -493,6 +578,8 @@ dependencies = [
name = "brass" name = "brass"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix-identity",
"actix-session",
"actix-web", "actix-web",
"anyhow", "anyhow",
"argon2", "argon2",
@ -565,6 +652,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.2.0" version = "2.2.0"
@ -586,7 +683,14 @@ version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [ dependencies = [
"aes-gcm",
"base64 0.20.0",
"hkdf",
"hmac",
"percent-encoding", "percent-encoding",
"rand",
"sha2",
"subtle",
"time", "time",
"version_check", "version_check",
] ]
@ -650,9 +754,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"rand_core",
"typenum", "typenum",
] ]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.17" version = "0.99.17"
@ -894,6 +1008,16 @@ dependencies = [
"wasi", "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]] [[package]]
name = "gloo-timers" name = "gloo-timers"
version = "0.2.6" version = "0.2.6"
@ -979,6 +1103,24 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 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]] [[package]]
name = "http" name = "http"
version = "0.2.9" version = "0.2.9"
@ -1031,6 +1173,15 @@ dependencies = [
"hashbrown 0.12.3", "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]] [[package]]
name = "instant" name = "instant"
version = "0.1.12" version = "0.1.12"
@ -1249,6 +1400,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "parking" name = "parking"
version = "2.1.0" version = "2.1.0"
@ -1380,6 +1537,18 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -1525,7 +1694,7 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
dependencies = [ dependencies = [
"base64", "base64 0.21.2",
] ]
[[package]] [[package]]
@ -1975,6 +2144,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" 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]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.7.1" version = "0.7.1"

View File

@ -10,6 +10,8 @@ sqlx = { version = "0.6", features = [ "runtime-async-std-rustls", "sqlite" ] }
actix-web = { version = "4" } actix-web = { version = "4" }
askama = "0.12.0" askama = "0.12.0"
serde = { version = "1.0.164", features = [ "derive"]} serde = { version = "1.0.164", features = [ "derive"]}
argon2 = "0.5.0" argon2 = { version = "0.5.0", features = [ "std"]}
anyhow = "1.0.71" anyhow = "1.0.71"
dotenv = "0.15.0" dotenv = "0.15.0"
actix-session = { version = "0.7.2", features = ["cookie-session"] }
actix-identity = "0.5.2"

View File

@ -9,9 +9,10 @@ INSERT OR REPLACE INTO roles(id, definition) values(2, "Administrator");
CREATE TABLE IF NOT EXISTS users CREATE TABLE IF NOT EXISTS users
( (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
password TEXT NOT NULL, password_hash TEXT NOT NULL,
role INTEGER NOT NULL, salt TEXT NOT NULL,
FOREIGN KEY(role) REFERENCES roles(roleId) role_id INTEGER NOT NULL,
FOREIGN KEY(role_id) REFERENCES roles(id)
); );

View File

@ -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<Role>
}
#[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<LoginForm>) -> 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<SqlitePool>) -> 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<LoginForm>) -> impl Responder {
println!("{} - {}", form.name, form.password);
let hash = hash_plain_password(&form.password);
println!("{hash}");
"dfgdg"
}

View File

@ -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<Identity>) -> impl Responder {
if let Some(_) = user {
return HttpResponse::PermanentRedirect()
.insert_header((LOCATION, "/"))
.finish();
} else {
let bla = LoginTemplate {};
return HttpResponse::Ok().body(bla.render().unwrap());
}
}

View File

@ -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<Role>,
}
#[actix_web::get("/register")]
async fn route(pool: web::Data<SqlitePool>) -> 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())
}

13
src/auth/routes/mod.rs Normal file
View File

@ -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);
}

View File

@ -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<LoginForm>,
request: HttpRequest,
pool: web::Data<SqlitePool>,
) -> 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."),
}
}

View File

@ -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<RegisterForm>,
pool: web::Data<SqlitePool>,
) -> 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()),
}
}

View File

@ -1,20 +1,25 @@
use argon2::{ use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, SaltString}, password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
Argon2, 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); let salt = SaltString::generate(&mut OsRng);
// Argon2 with default params (Argon2id v19) // Argon2 with default params (Argon2id v19)
Argon2::default() let hash = Argon2::default()
.hash_password(plain.as_bytes(), &salt) .hash_password(plain.as_bytes(), &salt)?
.unwrap() .to_string();
.to_string()
// Verify password against PHC string. Ok((hash, salt.to_string()))
// }
// NOTE: hash params from `parsed_hash` are used instead of what is configured in the
// `Argon2` instance. pub fn hash_plain_password_with_salt(plain: &str, salt_string: &str) -> anyhow::Result<String> {
// PasswordHash::new(&password_hash).unwrap().to_string() let salt = SaltString::from_b64(salt_string)?;
let hash = Argon2::default()
.hash_password(plain.as_bytes(), &salt)?
.to_string();
Ok(hash)
} }

3
src/calendar/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod routes;
pub use routes::init;

12
src/calendar/routes.rs Normal file
View File

@ -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!")
}

View File

@ -1,21 +1,34 @@
use std::env; use std::env;
use actix_web::{App, HttpServer, web}; use actix_identity::IdentityMiddleware;
use sqlx::sqlite::SqlitePool; use actix_session::{storage::CookieSessionStore, SessionMiddleware};
use actix_web::cookie::Key;
use actix_web::{web, App, HttpServer};
use dotenv::dotenv; use dotenv::dotenv;
use sqlx::sqlite::SqlitePool;
mod auth; mod auth;
mod calendar;
mod models; mod models;
#[actix_web::main] #[actix_web::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
dotenv()?; dotenv()?;
let pool = SqlitePool::connect(&env::var("DATABASE_URL")?).await?; let pool = SqlitePool::connect(&env::var("DATABASE_URL")?).await?;
let secret_key = Key::generate();
println!("Starting server on http://localhost:8080.");
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.app_data(web::Data::new(pool.clone())) .app_data(web::Data::new(pool.clone()))
.configure(auth::init) .configure(auth::init)
.configure(calendar::init)
.wrap(IdentityMiddleware::default())
.wrap(SessionMiddleware::new(
CookieSessionStore::default(),
secret_key.clone(),
))
}) })
.bind(("127.0.0.1", 8080))? .bind(("127.0.0.1", 8080))?
.run() .run()

View File

@ -1,3 +1,5 @@
pub mod user;
use sqlx::sqlite::SqlitePool; use sqlx::sqlite::SqlitePool;
pub struct Role { pub struct Role {

81
src/models/user.rs Normal file
View File

@ -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<i64> {
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<User> {
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<User> {
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
}
}
}
}