Prep for a "real" login system and sessions

This commit is contained in:
Daniel Flanagan 2024-05-20 17:00:23 -05:00
parent 305c1bd011
commit a9dd1f9316
6 changed files with 341 additions and 36 deletions

238
Cargo.lock generated
View file

@ -181,6 +181,26 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-login"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fbc0d7bd2577dda9aa9cac096e53b30342725d8eea5798169ff2537a214f45"
dependencies = [
"async-trait",
"axum",
"form_urlencoded",
"serde",
"subtle",
"thiserror",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions",
"tracing",
"urlencoding",
]
[[package]]
name = "axum-macros"
version = "0.4.1"
@ -214,6 +234,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[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"
@ -429,6 +455,17 @@ dependencies = [
"unicode-segmentation",
]
[[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-sys"
version = "0.8.6"
@ -493,6 +530,16 @@ dependencies = [
"typenum",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -963,6 +1010,7 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
"serde",
]
[[package]]
@ -977,6 +1025,7 @@ version = "0.1.0"
dependencies = [
"argon2",
"axum",
"axum-login",
"bincode",
"chrono",
"clap",
@ -993,6 +1042,8 @@ dependencies = [
"tokio",
"tower-http",
"tower-livereload",
"tower-sessions",
"tower-sessions-sled-store",
"tracing",
"tracing-subscriber",
"uuid",
@ -1124,6 +1175,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1239,6 +1296,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.1"
@ -1328,6 +1391,18 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@ -1369,6 +1444,27 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
@ -1458,13 +1554,35 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]]
name = "rmp"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
dependencies = [
"byteorder",
"num-traits",
"paste",
]
[[package]]
name = "rmp-serde"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
dependencies = [
"byteorder",
"rmp",
"serde",
]
[[package]]
name = "ron"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
dependencies = [
"base64",
"base64 0.21.7",
"bitflags 2.5.0",
"serde",
"serde_derive",
@ -1710,6 +1828,37 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
@ -1812,6 +1961,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",
"cookie",
"futures-util",
"http",
"parking_lot 0.12.2",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.5.2"
@ -1863,6 +2029,69 @@ 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",
"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",
"base64 0.22.1",
"futures",
"http",
"parking_lot 0.12.2",
"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 = "tower-sessions-sled-store"
version = "0.1.0"
source = "git+https://github.com/lytedev/tower-sessions-sled-store.git?branch=tower-sessions-0.12#95e9ffd89c971f0199003ed4c3b5cba2e2dba254"
dependencies = [
"async-trait",
"rmp-serde",
"sled",
"tokio",
"tower-sessions",
]
[[package]]
name = "tracing"
version = "0.1.40"
@ -1968,6 +2197,12 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf8parse"
version = "0.2.1"
@ -1982,6 +2217,7 @@ checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
dependencies = [
"atomic",
"getrandom",
"serde",
]
[[package]]

View file

@ -20,6 +20,7 @@ panic = "abort"
[dependencies]
argon2 = { version = "0.5.3", features = ["std"] }
axum-login = "0.15.1"
bincode = "1.3.3"
chrono = { version = "0.4.38", features = ["serde"] }
clap = { version = "4.5.4", features = ["derive", "env"] }
@ -36,9 +37,11 @@ thiserror = "1.0.60"
tokio = { version = "1.37.0", features = ["full"] }
tower-http = { version = "0.5.2", features = ["fs", "trace"] }
tower-livereload = "0.9.2"
tower-sessions = "0.12.2"
tower-sessions-sled-store = { git = "https://github.com/lytedev/tower-sessions-sled-store.git", branch = "tower-sessions-0.12" }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
uuid = { version = "1.8.0", features = ["v7"] }
uuid = { version = "1.8.0", features = ["v7", "serde"] }
walkdir = "2.5.0"
[dependencies.axum]

View file

@ -14,6 +14,7 @@ use axum::{
routing::get,
Router,
};
use axum_login::login_required;
use maud::html;
use sled::IVec;
use thiserror::Error;
@ -80,6 +81,8 @@ pub async fn router(
.route("/", get(index))
.route("/about", get(about))
.route("/users", get(users))
.route("/dashboard", get(dashboard))
.route_layer(login_required!(AppState, login_url = "/auth/login"))
.nest_service("/auth", auth_service)
.nest_service("/static", static_file_service)
.layer(TraceLayer::new_for_http())
@ -99,7 +102,11 @@ async fn index() -> ReqResult<Html<String>> {
}
async fn about() -> ReqResult<Html<String>> {
page("index", html! { "About" })
page("about", html! { "About" })
}
async fn dashboard() -> ReqResult<Html<String>> {
page("dashboard", html! { "Dashboard" })
}
async fn users(State(state): State<AppState>) -> ReqResult<String> {

View file

@ -1,8 +1,9 @@
use crate::router::ReqResult;
use crate::state::State as AppState;
use crate::state::{Creds, State as AppState};
use crate::{partials::*, user::User};
use axum::extract::State;
use axum::response::Html;
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse, Redirect};
use axum::Form;
use maud::html;
use redact::Secret;
@ -63,40 +64,24 @@ async fn register() -> ReqResult<Html<String>> {
page("login", center_hero_form("Register", form, subaction))
}
#[derive(Deserialize, Debug)]
struct Creds {
username: String,
password: Secret<String>,
}
type AuthSession = axum_login::AuthSession<AppState>;
#[instrument(skip(state))]
#[instrument(skip(auth_session))]
async fn authenticate(
State(state): State<AppState>,
mut auth_session: AuthSession,
Form(creds): Form<Creds>,
) -> ReqResult<Html<String>> {
let existing_user: Option<User> = state.db.get(User::tree(), &creds.username)?;
) -> impl IntoResponse {
let user = match auth_session.authenticate(creds.clone()).await {
Ok(Some(user)) => user,
Ok(None) => return StatusCode::UNAUTHORIZED.into_response(),
Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
};
if existing_user.is_none() {
// timing/enumeration attacks or something
return Err(user::Error::UsernameNotFound(Box::new(creds.username)).into());
if auth_session.login(&user).await.is_err() {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
let existing_user = existing_user.unwrap();
if let Err(err) = existing_user.verify(creds.password.expose_secret()) {
Ok(Html(
html! {
"failed to login: " ({err})
}
.into_string(),
))
} else {
Ok(Html(
html! {
"logged in"
}
.into_string(),
))
}
Redirect::to("/dashboard").into_response()
}
#[instrument(skip(state))]

View file

@ -1,7 +1,12 @@
use crate::{
db::{self, Data},
prelude::*,
user::{self, User},
};
use axum::async_trait;
use axum_login::{AuthnBackend, UserId};
use redact::Secret;
use serde::Deserialize;
use thiserror::Error;
#[derive(Clone)]
@ -22,3 +27,50 @@ pub enum NewStateError {
#[error("database error: {0}")]
Database(#[from] db::Error),
}
#[derive(Deserialize, Debug, Clone)]
pub struct Creds {
pub username: String,
pub password: Secret<String>,
}
#[derive(Error, Debug)]
pub enum AuthError {
#[error("user error: {0}")]
User(#[from] user::Error),
#[error("data error: {0}")]
Db(#[from] db::Error),
#[error("data error: {0}")]
Argon2(#[from] argon2::password_hash::Error),
}
#[async_trait]
impl AuthnBackend for State {
type User = User;
type Credentials = Creds;
type Error = AuthError;
async fn authenticate(
&self,
Creds { username, password }: Self::Credentials,
) -> Result<Option<Self::User>, Self::Error> {
if let Some(user) = self.db.get::<String, User>(User::tree(), username)? {
if let Err(err) = user.verify(password.expose_secret()) {
Err(err.into())
} else {
Ok(Some(user))
}
} else {
Ok(None)
}
}
async fn get_user(&self, username: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
match self.db.get::<&str, User>(User::tree(), username)? {
Some(user) => Ok(Some(user)),
None => Ok(None),
}
}
}

View file

@ -1,17 +1,20 @@
use crate::prelude::*;
use axum_login::AuthUser;
use chrono::Utc;
use redact::{expose_secret, Secret};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::{NoContext, Uuid};
pub const USER_TREE: &str = "user";
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct User {
pub id: Uuid,
pub username: String,
#[serde(serialize_with = "expose_secret")]
password_digest: Secret<Vec<u8>>,
password_digest: Secret<String>,
pub registered_at: chrono::DateTime<Utc>,
}
@ -31,9 +34,16 @@ impl User {
}
pub fn try_new(username: &str, password: &str) -> Result<Self, argon2::password_hash::Error> {
let now = Utc::now();
let ts = uuid::Timestamp::from_unix(
NoContext,
u64::from_ne_bytes(now.timestamp().to_ne_bytes()),
now.timestamp_subsec_micros(),
);
Ok(Self {
id: Uuid::new_v7(ts),
username: username.to_owned(),
registered_at: Utc::now(),
registered_at: now,
password_digest: Secret::new(crate::auth::password_digest(password)?.into()),
})
}
@ -49,3 +59,15 @@ impl TryFrom<sled::IVec> for User {
bincode::deserialize(&value)
}
}
impl AuthUser for User {
type Id = String;
fn id(&self) -> Self::Id {
self.username.clone()
}
fn session_auth_hash(&self) -> &[u8] {
self.password_digest.expose_secret().as_bytes()
}
}