diff --git a/Cargo.lock b/Cargo.lock index 9e35bb5..fb4f0dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,18 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -317,9 +329,9 @@ checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46" [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", @@ -458,6 +470,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-login" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4012877d9672b7902aa6567960208756f68a09de81e988fa18fe369e92f90471" +dependencies = [ + "async-trait", + "axum 0.7.4", + "form_urlencoded", + "serde", + "subtle", + "thiserror", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions", + "tracing", + "urlencoding", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -485,6 +517,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -529,6 +567,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -725,6 +772,17 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -743,9 +801,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -1018,9 +1076,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1126,6 +1184,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "futures-sink" version = "0.3.29" @@ -1147,6 +1216,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1181,8 +1251,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1650,6 +1722,7 @@ checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -1769,6 +1842,7 @@ dependencies = [ "minijinja-autoreload", "once_cell", "rear", + "rear_auth", "rust-embed", "sea-orm", "serde", @@ -2047,6 +2121,29 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "password-auth" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2a4764cc1f8d961d802af27193c6f4f0124bd0e76e8393cf818e18880f0524" +dependencies = [ + "argon2", + "getrandom", + "password-hash", + "rand_core", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" @@ -2325,6 +2422,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "rear_auth" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum 0.7.4", + "axum-login", + "entity", + "log", + "password-auth", + "rear", + "sea-orm", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -2763,18 +2878,18 @@ checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -3057,7 +3172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" dependencies = [ "atoi", - "base64", + "base64 0.21.5", "bigdecimal", "bitflags 2.4.0", "byteorder", @@ -3104,7 +3219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" dependencies = [ "atoi", - "base64", + "base64 0.21.5", "bigdecimal", "bitflags 2.4.0", "byteorder", @@ -3424,6 +3539,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-cookies" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "cookie", + "futures-util", + "http 1.0.0", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.5.1" @@ -3453,6 +3585,57 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +[[package]] +name = "tower-sessions" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d9b6f0c4938eed0eefd9cce19319b4bdad10e11ca9d8c3be373ce734bbfd63" +dependencies = [ + "async-trait", + "http 1.0.0", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions-core", + "tower-sessions-memory-store", + "tracing", +] + +[[package]] +name = "tower-sessions-core" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38767064990c327ec1d92bba2576dce0944750e9c9ae021f12ebc72de77ac406" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "base64 0.22.1", + "futures", + "http 1.0.0", + "parking_lot", + "rand", + "serde", + "serde_json", + "thiserror", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8b09bbe2c138a9b0ebf307dc6e6a4f7723c59545e0f4fe5e329a89868164ae3" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + [[package]] name = "tracing" version = "0.1.40" diff --git a/Cargo.toml b/Cargo.toml index b75f77d..088fe61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ default-run = "miniweb" [workspace] -members = [".", "entity", "migration", "rear"] +members = [".", "entity", "migration", "rear", "rear_auth"] [features] # https://github.com/rust-db/barrel/blob/master/guides/diesel-setup.md @@ -20,6 +20,7 @@ default = ["use_barrel"] # strinto = { path = "./strinto" } entity = { path = "./entity" } rear = { path = "./rear" } +rear_auth = { path = "./rear_auth" } sea-orm = { version = "0.12.10", features = [ "runtime-tokio-native-tls", "sqlx-postgres", diff --git a/entity/src/user.rs b/entity/src/user.rs index 2c49a84..9373c5a 100644 --- a/entity/src/user.rs +++ b/entity/src/user.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; pub struct Model { #[sea_orm(primary_key)] #[serde(skip_deserializing)] - pub id: i32, + pub id: i64, pub username: String, #[sea_orm(column_type = "Text")] pub description: Option, diff --git a/rear/src/auth/mod.rs b/rear/src/auth/mod.rs deleted file mode 100644 index e072fd8..0000000 --- a/rear/src/auth/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod models; diff --git a/rear/src/lib.rs b/rear/src/lib.rs index 01b001c..8459cac 100644 --- a/rear/src/lib.rs +++ b/rear/src/lib.rs @@ -1,3 +1,2 @@ pub mod admin; -pub mod auth; pub mod service; diff --git a/rear_auth/Cargo.toml b/rear_auth/Cargo.toml new file mode 100644 index 0000000..acc4b41 --- /dev/null +++ b/rear_auth/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "rear_auth" +version = "0.1.0" +edition = "2021" +publish = false +default-run = "rear_auth" + +[lib] +name = "rear_auth" +path = "src/lib.rs" + +[dependencies] +entity = { path = "../entity" } +rear = { path = "../rear" } +anyhow = "1.0.75" +axum = "0.7" +serde = { version = "1.0.188", features = ["derive"] } +tokio = { version = "1.32.0", features = ["full"] } +log = "0.4.20" +serde_json = "1.0.108" +sea-orm = { version = "0.12.10", features = [ + "runtime-tokio-native-tls", + "sqlx-postgres", +] } +axum-login = "0.15.3" +async-trait = "0.1.80" +password-auth = "1.0.0" diff --git a/rear_auth/src/lib.rs b/rear_auth/src/lib.rs new file mode 100644 index 0000000..c8435ce --- /dev/null +++ b/rear_auth/src/lib.rs @@ -0,0 +1 @@ +pub mod users; diff --git a/rear_auth/src/main.rs b/rear_auth/src/main.rs new file mode 100644 index 0000000..a9c7ed9 --- /dev/null +++ b/rear_auth/src/main.rs @@ -0,0 +1,3 @@ +mod users; + +pub fn main() {} diff --git a/rear_auth/src/users.rs b/rear_auth/src/users.rs new file mode 100644 index 0000000..999d925 --- /dev/null +++ b/rear_auth/src/users.rs @@ -0,0 +1,108 @@ +use async_trait::async_trait; +use axum_login::{AuthUser, AuthnBackend, UserId}; +use password_auth::verify_password; +use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set}; +use serde::{Deserialize, Serialize}; +use tokio::task; + +struct UserDatabase { + connection: DatabaseConnection, +} + +impl UserDatabase { + pub fn new(connection: DatabaseConnection) -> Self { + UserDatabase { + connection: connection, + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct User { + id: i64, + pub username: String, + password: String, +} + +// Here we've implemented `Debug` manually to avoid accidentally logging the +// password hash. +impl std::fmt::Debug for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("User") + .field("id", &self.id) + .field("username", &self.username) + .field("password", &"[redacted]") + .finish() + } +} + +impl AuthUser for User { + type Id = i64; + + fn id(&self) -> Self::Id { + self.id + } + + fn session_auth_hash(&self) -> &[u8] { + self.password.as_bytes() // We use the password hash as the auth + // hash--what this means + // is when the user changes their password the + // auth session becomes invalid. + } +} + +// This allows us to extract the authentication fields from forms. We use this +// to authenticate requests with the backend. +#[derive(Debug, Clone, Deserialize)] +pub struct Credentials { + pub username: String, + pub password: String, + pub next: Option, +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Sqlx(#[from] sqlx::Error), + + #[error(transparent)] + TaskJoin(#[from] task::JoinError), +} + +#[async_trait] +impl AuthnBackend for UserDatabase { + type User = User; + type Credentials = Credentials; + type Error = Error; + + async fn authenticate( + &self, + creds: Self::Credentials, + ) -> Result, Self::Error> { + let user: Option = sqlx::query_as("select * from users where username = ? ") + .bind(creds.username) + .fetch_optional(&self.db) + .await?; + + // Verifying the password is blocking and potentially slow, so we'll do so via + // `spawn_blocking`. + task::spawn_blocking(|| { + // We're using password-based authentication--this works by comparing our form + // input with an argon2 password hash. + Ok(user.filter(|user| verify_password(creds.password, &user.password).is_ok())) + }) + .await? + } + + async fn get_user(&self, user_id: &UserId) -> Result, Self::Error> { + let user = entity::User::find_by_id(*user_id) + .one(&self.connection) + .await?; + Ok(user) + } +} + +// We use a type alias for convenience. +// +// Note that we've supplied our concrete backend here. +pub type AuthSession = axum_login::AuthSession;