Now that I've rolled my own crappy auth

This commit is contained in:
Daniel Flanagan 2024-05-20 16:03:37 -05:00
parent 0879a84df1
commit 305c1bd011
6 changed files with 283 additions and 29 deletions

157
Cargo.lock generated
View file

@ -26,6 +26,21 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.14" version = "0.6.14"
@ -205,6 +220,15 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -238,6 +262,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@ -262,6 +292,21 @@ 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 = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.5",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.4" version = "4.5.4"
@ -384,6 +429,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.12" version = "0.2.12"
@ -766,6 +817,29 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "indenter" name = "indenter"
version = "0.3.3" version = "0.3.3"
@ -823,6 +897,15 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
[[package]] [[package]]
name = "json5" name = "json5"
version = "0.4.1" version = "0.4.1"
@ -894,6 +977,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"argon2", "argon2",
"axum", "axum",
"bincode",
"chrono",
"clap", "clap",
"color-eyre", "color-eyre",
"config", "config",
@ -1039,6 +1124,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.16.0"
@ -1918,6 +2012,60 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -1949,6 +2097,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View file

@ -20,6 +20,8 @@ panic = "abort"
[dependencies] [dependencies]
argon2 = { version = "0.5.3", features = ["std"] } argon2 = { version = "0.5.3", features = ["std"] }
bincode = "1.3.3"
chrono = { version = "0.4.38", features = ["serde"] }
clap = { version = "4.5.4", features = ["derive", "env"] } clap = { version = "4.5.4", features = ["derive", "env"] }
color-eyre = "0.6.3" color-eyre = "0.6.3"
config = "0.14.0" config = "0.14.0"

View file

@ -1,3 +1,4 @@
use serde::de::DeserializeOwned;
use sled::{Db, IVec}; use sled::{Db, IVec};
use thiserror::Error; use thiserror::Error;
@ -10,6 +11,9 @@ pub struct Data {
pub enum Error { pub enum Error {
#[error("sled error: {0}")] #[error("sled error: {0}")]
Sled(#[from] sled::Error), Sled(#[from] sled::Error),
#[error("bincode error: {0}")]
Binccode(#[from] Box<bincode::ErrorKind>),
} }
impl Data { impl Data {
@ -19,16 +23,15 @@ impl Data {
}) })
} }
pub fn get<K: AsRef<[u8]>, V: From<IVec>>( pub fn get<K, V>(&self, tree_name: &str, key: K) -> Result<Option<V>, Error>
&self, where
tree_name: &str, K: AsRef<[u8]>,
key: K, V: DeserializeOwned,
) -> Result<Option<V>, Error> { {
Ok(self match self.db.open_tree(tree_name)?.get(key.as_ref())? {
.db Some(v) => Ok(Some(bincode::deserialize::<V>(&v)?)),
.open_tree(tree_name)? None => Ok(None),
.get(key.as_ref())? }
.map(V::from))
} }
pub fn insert<K: AsRef<[u8]>, V: Into<IVec>>( pub fn insert<K: AsRef<[u8]>, V: Into<IVec>>(
@ -39,4 +42,29 @@ impl Data {
) -> Result<Option<IVec>, Error> { ) -> Result<Option<IVec>, Error> {
Ok(self.db.open_tree(tree_name)?.insert(key, value.into())?) Ok(self.db.open_tree(tree_name)?.insert(key, value.into())?)
} }
pub fn all<'de, K, V>(
&self,
tree_name: &str,
) -> Result<impl Iterator<Item = Result<(K, V), Error>>, Error>
where
V: DeserializeOwned,
K: From<IVec>,
{
Ok(self
.db
.open_tree(tree_name)?
.scan_prefix([])
.map(|r| match r {
Ok((k, v)) => {
let key = K::from(k);
match bincode::deserialize::<V>(&v).map_err(Error::from) {
Ok(v) => Ok((key, v)),
Err(err) => Err(Error::from(err)),
}
}
Err(err) => Err(Error::from(err)),
})
.into_iter())
}
} }

View file

@ -1,10 +1,13 @@
use crate::partials::page; use crate::partials::page;
use crate::user::User;
use crate::{db, user};
use crate::{ use crate::{
file_watcher::FileWatcher, file_watcher::FileWatcher,
prelude::*, prelude::*,
service::{auth, static_files}, service::{auth, static_files},
state::State as AppState, state::State as AppState,
}; };
use axum::extract::State;
use axum::{ use axum::{
http::StatusCode, http::StatusCode,
response::{Html, IntoResponse}, response::{Html, IntoResponse},
@ -12,6 +15,7 @@ use axum::{
Router, Router,
}; };
use maud::html; use maud::html;
use sled::IVec;
use thiserror::Error; use thiserror::Error;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tower_livereload::LiveReloadLayer; use tower_livereload::LiveReloadLayer;
@ -26,6 +30,15 @@ pub enum NewRouterError {
pub enum ReqError { pub enum ReqError {
#[error("argon2 error: {0}")] #[error("argon2 error: {0}")]
Argon2(#[from] argon2::password_hash::Error), Argon2(#[from] argon2::password_hash::Error),
#[error("bincode error: {0}")]
Bincode(#[from] bincode::Error),
#[error("database error: {0}")]
Database(#[from] db::Error),
#[error("user error: {0}")]
User(#[from] user::Error),
} }
impl IntoResponse for ReqError { impl IntoResponse for ReqError {
@ -61,11 +74,12 @@ pub async fn router(
}; };
let (static_file_service, static_file_watcher) = static_files::router(orl())?; let (static_file_service, static_file_watcher) = static_files::router(orl())?;
let auth_service = auth::router().unwrap(); let auth_service = auth::router(state.clone()).unwrap();
let mut result = Router::new() let mut result = Router::new()
.route("/", get(index)) .route("/", get(index))
.route("/about", get(about)) .route("/about", get(about))
.route("/users", get(users))
.nest_service("/auth", auth_service) .nest_service("/auth", auth_service)
.nest_service("/static", static_file_service) .nest_service("/static", static_file_service)
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
@ -87,3 +101,12 @@ async fn index() -> ReqResult<Html<String>> {
async fn about() -> ReqResult<Html<String>> { async fn about() -> ReqResult<Html<String>> {
page("index", html! { "About" }) page("index", html! { "About" })
} }
async fn users(State(state): State<AppState>) -> ReqResult<String> {
let mut s = String::new();
let mut users = state.db.all::<IVec, User>(User::tree())?;
while let Some(Ok((_, user))) = users.next() {
s.push_str(&format!("{}: {:?}", user.username, user.registered_at))
}
Ok(s)
}

View file

@ -14,7 +14,7 @@ use axum::{
Router, Router,
}; };
use crate::prelude::*; use crate::{prelude::*, user};
pub fn router(state: AppState) -> Result<Router, Infallible> { pub fn router(state: AppState) -> Result<Router, Infallible> {
Ok(Router::new() Ok(Router::new()
@ -37,7 +37,7 @@ async fn login() -> ReqResult<Html<String>> {
let subaction = html! { let subaction = html! {
small class="mt-4" { small class="mt-4" {
"Need an account? " "Need an account? "
a href="/register" {"Get one"} a href="/auth/register" {"Get one"}
"." "."
} }
}; };
@ -56,7 +56,7 @@ async fn register() -> ReqResult<Html<String>> {
let subaction = html! { let subaction = html! {
small class="mt-4" { small class="mt-4" {
"Already have an account? " "Already have an account? "
a href="/login" {"Login"} a href="/auth/login" {"Login"}
"." "."
} }
}; };
@ -69,15 +69,34 @@ struct Creds {
password: Secret<String>, password: Secret<String>,
} }
#[instrument] #[instrument(skip(state))]
async fn authenticate(Form(creds): Form<Creds>) -> ReqResult<Html<String>> { async fn authenticate(
info!("login attempt"); State(state): State<AppState>,
Form(creds): Form<Creds>,
) -> ReqResult<Html<String>> {
let existing_user: Option<User> = state.db.get(User::tree(), &creds.username)?;
if existing_user.is_none() {
// timing/enumeration attacks or something
return Err(user::Error::UsernameNotFound(Box::new(creds.username)).into());
}
let existing_user = existing_user.unwrap();
if let Err(err) = existing_user.verify(creds.password.expose_secret()) {
Ok(Html( Ok(Html(
html! { html! {
"no" "failed to login: " ({err})
} }
.into_string(), .into_string(),
)) ))
} else {
Ok(Html(
html! {
"logged in"
}
.into_string(),
))
}
} }
#[instrument(skip(state))] #[instrument(skip(state))]
@ -86,9 +105,21 @@ async fn create_user(
Form(creds): Form<Creds>, Form(creds): Form<Creds>,
) -> ReqResult<Html<String>> { ) -> ReqResult<Html<String>> {
let user = User::try_new(&creds.username, creds.password.expose_secret())?; let user = User::try_new(&creds.username, creds.password.expose_secret())?;
let existing_user: Option<User> = state.db.get(User::tree(), &creds.username)?;
// TODO: fail2ban?
if existing_user.is_some() {
// timing/enumeration attacks or something
return Err(user::Error::UsernameExists(Box::new(creds.username)).into());
}
state
.db
.insert(User::tree(), &user.username, bincode::serialize(&user)?)?;
Ok(Html( Ok(Html(
html! { html! {
({user.username}) " has been registered" ({&user.username}) " has been registered"
} }
.into_string(), .into_string(),
)) ))

View file

@ -1,14 +1,19 @@
use crate::prelude::*; use crate::prelude::*;
use redact::Secret; use chrono::Utc;
use serde::Deserialize; use redact::{expose_secret, Secret};
use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
pub const USER_TREE: &str = "user"; pub const USER_TREE: &str = "user";
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct User { pub struct User {
pub username: String, pub username: String,
password_digest: Secret<String>,
#[serde(serialize_with = "expose_secret")]
password_digest: Secret<Vec<u8>>,
pub registered_at: chrono::DateTime<Utc>,
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -28,7 +33,8 @@ impl User {
pub fn try_new(username: &str, password: &str) -> Result<Self, argon2::password_hash::Error> { pub fn try_new(username: &str, password: &str) -> Result<Self, argon2::password_hash::Error> {
Ok(Self { Ok(Self {
username: username.to_owned(), username: username.to_owned(),
password_digest: Secret::new(crate::auth::password_digest(password)?.to_string()), registered_at: Utc::now(),
password_digest: Secret::new(crate::auth::password_digest(password)?.into()),
}) })
} }
@ -36,3 +42,10 @@ impl User {
crate::auth::verified_password(password, self.password_digest.expose_secret()) crate::auth::verified_password(password, self.password_digest.expose_secret())
} }
} }
impl TryFrom<sled::IVec> for User {
type Error = bincode::Error;
fn try_from(value: sled::IVec) -> Result<Self, Self::Error> {
bincode::deserialize(&value)
}
}