Merge pull request 'auth' (#28) from auth into main
Some checks failed
CI/CD Pipeline / test (push) Failing after 6s
CI/CD Pipeline / deploy (push) Has been skipped

Reviewed-on: #28
This commit is contained in:
philipp 2025-04-12 19:49:46 +02:00
commit 364b5d0863
9 changed files with 412 additions and 9 deletions

211
Cargo.lock generated
View File

@ -59,6 +59,18 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[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 = "assert-json-diff"
version = "2.0.2"
@ -155,6 +167,26 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-login"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0da8e8e4cf127a9b71b578e9a8fa9833e70f893e428ed5453c85e44bf0fd8eb"
dependencies = [
"async-trait",
"axum",
"form_urlencoded",
"serde",
"subtle",
"thiserror",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions",
"tracing",
"urlencoding",
]
[[package]]
name = "axum-test"
version = "17.3.0"
@ -236,6 +268,15 @@ dependencies = [
"serde",
]
[[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"
@ -671,8 +712,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@ -1224,6 +1267,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
@ -1292,6 +1345,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking"
version = "2.2.1"
@ -1321,6 +1380,35 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "password-auth"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a2a4764cc1f8d961d802af27193c6f4f0124bd0e76e8393cf818e18880f0524"
dependencies = [
"argon2",
"getrandom 0.2.15",
"password-hash",
"rand_core 0.6.4",
]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
@ -1557,6 +1645,28 @@ dependencies = [
"windows-sys 0.52.0",
]
[[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 = "rsa"
version = "0.9.8"
@ -1830,6 +1940,15 @@ dependencies = [
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -2103,17 +2222,23 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
name = "stationslauf"
version = "0.1.0"
dependencies = [
"async-trait",
"axum",
"axum-login",
"axum-test",
"chrono",
"dotenv",
"maud",
"password-auth",
"rust-i18n",
"serde",
"sqlx",
"thiserror",
"tokio",
"tower-sessions",
"tower-sessions-sqlx-store-chrono",
"tracing",
"tracing-subscriber",
]
[[package]]
@ -2194,6 +2319,16 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "time"
version = "0.3.41"
@ -2417,6 +2552,22 @@ dependencies = [
"tower-sessions-core",
]
[[package]]
name = "tower-sessions-sqlx-store-chrono"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b295c8fc08db03246e92773c5e10119b72db6bc4240112135bebb0e49670804f"
dependencies = [
"async-trait",
"axum",
"chrono",
"rmp-serde",
"sqlx",
"thiserror",
"time",
"tower-sessions-core",
]
[[package]]
name = "tracing"
version = "0.1.41"
@ -2447,6 +2598,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"nu-ansi-term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
]
[[package]]
@ -2522,6 +2699,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf16_iter"
version = "1.0.5"
@ -2534,6 +2717,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vcpkg"
version = "0.2.15"
@ -2663,6 +2852,22 @@ dependencies = [
"wasite",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
@ -2672,6 +2877,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.61.0"

View File

@ -5,6 +5,7 @@ edition = "2024"
[dependencies]
axum = "0.8"
axum-login = "0.17"
chrono = { version = "0.4", features = ["serde"]}
dotenv = "0.15"
maud = { version = "0.27", features = ["axum"] }
@ -14,6 +15,12 @@ tokio = { version = "1.44", features = ["macros", "rt-multi-thread"] }
tower-sessions = "0.14"
tracing = "0.1"
rust-i18n = "3"
thiserror = "2.0"
async-trait = "0.1"
password-auth = "1.0"
tower-sessions-sqlx-store-chrono = { version = "0.14", features = ["sqlite"] }
tracing-subscriber = "0.3"
[dev-dependencies]
axum-test = "17.3"

View File

@ -47,4 +47,15 @@ CREATE TABLE IF NOT EXISTS rating (
FOREIGN KEY (station_id) REFERENCES station(id)
);
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL UNIQUE,
pw TEXT NOT NULL
);
create table if not exists "tower_sessions" (
id text primary key not null,
data blob not null,
expiry_date integer not null
);

1
seeds.sql Normal file
View File

@ -0,0 +1 @@
insert into user(name, pw) values('a', '$argon2i$v=19$m=16,t=2,p=1$b2lmaG9pMzJvNDk$vXbHg45vkuMrQaP0XY184Q'); // pw = 123

View File

@ -1,6 +1,7 @@
use crate::{AppState, page};
use axum::{Router, routing::get};
use maud::{Markup, html};
use crate::{auth::Backend, page, AppState};
use axum::{routing::get, Router};
use axum_login::login_required;
use maud::{html, Markup};
use tower_sessions::Session;
pub(crate) mod route;
@ -10,6 +11,9 @@ pub(crate) mod team;
async fn index(session: Session) -> Markup {
let content = html! {
h1 { (t!("app_name")) }
a href="/auth/logout" {
"Ausloggen"
}
nav {
ul {
li {
@ -39,4 +43,5 @@ pub(super) fn routes() -> Router<AppState> {
.nest("/station", station::routes())
.nest("/route", route::routes())
.nest("/team", team::routes())
.layer(login_required!(Backend, login_url = "/auth/login"))
}

150
src/auth.rs Normal file
View File

@ -0,0 +1,150 @@
use crate::{err, page, succ, AppState};
use async_trait::async_trait;
use axum::{
http::StatusCode,
response::{IntoResponse, Redirect},
routing::{get, post},
Form, Router,
};
use axum_login::{AuthUser, AuthnBackend};
use maud::{html, Markup};
use password_auth::verify_password;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
use tower_sessions::Session;
pub type UserId<Backend> = <<Backend as AuthnBackend>::User as AuthUser>::Id;
#[derive(Clone, Serialize, Deserialize, FromRow, Debug)]
pub struct User {
id: i64,
name: String,
pw: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Credentials {
pub name: String,
pub password: String,
}
impl AuthUser for User {
type Id = i64;
fn id(&self) -> Self::Id {
self.id
}
fn session_auth_hash(&self) -> &[u8] {
&self.pw.as_bytes()
}
}
#[derive(Debug, Clone)]
pub struct Backend {
db: SqlitePool,
}
impl Backend {
pub(crate) fn new(db: SqlitePool) -> Self {
Self { db }
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Sqlx(#[from] sqlx::Error),
}
#[async_trait]
impl AuthnBackend for Backend {
type User = User;
type Credentials = Credentials;
type Error = Error;
async fn authenticate(
&self,
creds: Self::Credentials,
) -> Result<Option<Self::User>, Self::Error> {
let user: Option<Self::User> =
sqlx::query_as("SELECT id, name, pw FROM user WHERE name = ? ")
.bind(creds.name)
.fetch_optional(&self.db)
.await?;
// 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.pw).is_ok()))
}
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
let user = sqlx::query_as("SELECT id, name, pw FROM user WHERE id = ?")
.bind(user_id)
.fetch_optional(&self.db)
.await?;
Ok(user)
}
}
pub type AuthSession = axum_login::AuthSession<Backend>;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/login", get(self::login))
.route("/login", post(self::login_post))
.route("/logout", get(self::logout))
}
async fn login(session: Session) -> Markup {
let content = html! {
h1 { "Login" }
form action="/auth/login" method="post" {
label {
"Name"
input type="text" name="name";
}
label {
"Passwort"
input type="password" name="password";
}
input type="submit" value="Einloggen";
}
};
// TODO: generate okayish looking login page
page(content, session, false).await
}
pub async fn login_post(
mut auth_session: AuthSession,
session: Session,
Form(creds): Form<Credentials>,
) -> impl IntoResponse {
let user = match auth_session.authenticate(creds.clone()).await {
Ok(Some(user)) => user,
Ok(None) => {
err!(session, "Invalid credentials");
return Redirect::to("/auth/login").into_response();
}
Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
};
if auth_session.login(&user).await.is_err() {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
succ!(session, "Successfully logged in as {}", user.name);
Redirect::to("/admin").into_response()
}
pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
match auth_session.logout().await {
Ok(_) => Redirect::to("/auth/login").into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}

View File

@ -17,14 +17,18 @@ macro_rules! testdb {
i18n!("locales", fallback = "de-AT");
use admin::station::Station;
use axum::{Router, body::Body, extract::FromRef, response::Response, routing::get};
use auth::Backend;
use axum::{body::Body, extract::FromRef, response::Response, routing::get, Router};
use axum_login::AuthManagerLayerBuilder;
use partials::page;
use sqlx::SqlitePool;
use std::sync::Arc;
use tokio::net::TcpListener;
use tower_sessions::{MemoryStore, SessionManagerLayer};
use tower_sessions::{cookie::time::Duration, Expiry, SessionManagerLayer};
use tower_sessions_sqlx_store_chrono::SqliteStore;
pub(crate) mod admin;
mod auth;
pub(crate) mod models;
mod partials;
pub(crate) mod station;
@ -128,21 +132,28 @@ impl FromRef<AppState> for Arc<SqlitePool> {
}
fn router(db: SqlitePool) -> Router {
let session_store = MemoryStore::default();
let session_layer = SessionManagerLayer::new(session_store);
let session_store = SqliteStore::new(db.clone());
let session_layer = SessionManagerLayer::new(session_store)
.with_secure(false)
.with_expiry(Expiry::OnInactivity(Duration::weeks(2)));
let backend = Backend::new(db.clone());
let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
let state = AppState { db: Arc::new(db) };
Router::new()
.nest("/s/{id}/{code}", station::routes()) // TODO: maybe switch to "/"
.nest("/admin", admin::routes())
.nest("/auth", auth::routes())
.route("/pico.css", get(serve_pico_css))
.route("/style.css", get(serve_my_css))
.route("/leaflet.css", get(serve_leaflet_css))
.route("/leaflet.js", get(serve_leaflet_js))
.route("/marker.png", get(serve_marker_png))
.with_state(state)
.layer(session_layer)
.layer(auth_layer)
}
/// Starts the main application.

View File

@ -1,11 +1,17 @@
use dotenv::dotenv;
use sqlx::{SqlitePool, pool::PoolOptions};
use sqlx::{pool::PoolOptions, SqlitePool};
use std::env;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() {
dotenv().ok(); // load .env variables
// Logging
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.init();
// DB
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let db: SqlitePool = PoolOptions::new().connect(&database_url).await.unwrap();

View File

@ -3,4 +3,5 @@
rm -f db.sqlite
touch db.sqlite
sqlite3 db.sqlite < migration.sql
sqlite3 db.sqlite < seeds.sql