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-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"

View File

@ -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"

View File

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

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::{
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<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 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()

View File

@ -1,3 +1,5 @@
pub mod user;
use sqlx::sqlite::SqlitePool;
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
}
}
}
}