first draft of auth
This commit is contained in:
parent
26f7ec9237
commit
127d941d5d
121
Cargo.lock
generated
121
Cargo.lock
generated
@ -59,6 +59,18 @@ version = "1.7.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
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]]
|
[[package]]
|
||||||
name = "assert-json-diff"
|
name = "assert-json-diff"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@ -155,6 +167,26 @@ dependencies = [
|
|||||||
"tracing",
|
"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]]
|
[[package]]
|
||||||
name = "axum-test"
|
name = "axum-test"
|
||||||
version = "17.3.0"
|
version = "17.3.0"
|
||||||
@ -236,6 +268,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@ -671,8 +712,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1321,6 +1364,35 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "pem-rfc7468"
|
name = "pem-rfc7468"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@ -1557,6 +1629,28 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
@ -2103,16 +2197,21 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
|||||||
name = "stationslauf"
|
name = "stationslauf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
|
"axum-login",
|
||||||
"axum-test",
|
"axum-test",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"maud",
|
"maud",
|
||||||
|
"password-auth",
|
||||||
"rust-i18n",
|
"rust-i18n",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-sessions",
|
"tower-sessions",
|
||||||
|
"tower-sessions-sqlx-store-chrono",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2417,6 +2516,22 @@ dependencies = [
|
|||||||
"tower-sessions-core",
|
"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]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.41"
|
version = "0.1.41"
|
||||||
@ -2522,6 +2637,12 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf16_iter"
|
name = "utf16_iter"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
@ -5,6 +5,7 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
|
axum-login = "0.17"
|
||||||
chrono = { version = "0.4", features = ["serde"]}
|
chrono = { version = "0.4", features = ["serde"]}
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
maud = { version = "0.27", features = ["axum"] }
|
maud = { version = "0.27", features = ["axum"] }
|
||||||
@ -14,6 +15,11 @@ tokio = { version = "1.44", features = ["macros", "rt-multi-thread"] }
|
|||||||
tower-sessions = "0.14"
|
tower-sessions = "0.14"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
rust-i18n = "3"
|
rust-i18n = "3"
|
||||||
|
thiserror = "2.0"
|
||||||
|
async-trait = "0.1"
|
||||||
|
password-auth = "1.0"
|
||||||
|
tower-sessions-sqlx-store-chrono = { version = "0.14", features = ["sqlite"] }
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
axum-test = "17.3"
|
axum-test = "17.3"
|
||||||
|
@ -47,4 +47,15 @@ CREATE TABLE IF NOT EXISTS rating (
|
|||||||
FOREIGN KEY (station_id) REFERENCES station(id)
|
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,6 +1,7 @@
|
|||||||
use crate::{AppState, page};
|
use crate::{auth::Backend, page, AppState};
|
||||||
use axum::{Router, routing::get};
|
use axum::{routing::get, Router};
|
||||||
use maud::{Markup, html};
|
use axum_login::login_required;
|
||||||
|
use maud::{html, Markup};
|
||||||
use tower_sessions::Session;
|
use tower_sessions::Session;
|
||||||
|
|
||||||
pub(crate) mod route;
|
pub(crate) mod route;
|
||||||
@ -39,4 +40,5 @@ pub(super) fn routes() -> Router<AppState> {
|
|||||||
.nest("/station", station::routes())
|
.nest("/station", station::routes())
|
||||||
.nest("/route", route::routes())
|
.nest("/route", route::routes())
|
||||||
.nest("/team", team::routes())
|
.nest("/team", team::routes())
|
||||||
|
.layer(login_required!(Backend, login_url = "/auth/login"))
|
||||||
}
|
}
|
||||||
|
150
src/auth.rs
Normal file
150
src/auth.rs
Normal 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,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct Credentials {
|
||||||
|
pub name: String,
|
||||||
|
pub password: String,
|
||||||
|
pub next: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthUser for User {
|
||||||
|
type Id = i64;
|
||||||
|
|
||||||
|
fn id(&self) -> Self::Id {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_auth_hash(&self) -> &[u8] {
|
||||||
|
&self.password.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.password).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" }
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: generate okayish looking login page
|
||||||
|
|
||||||
|
page(content, session, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
let mut login_url = "/auth/login".to_string();
|
||||||
|
if let Some(next) = creds.next {
|
||||||
|
login_url = format!("{login_url}?next={next}");
|
||||||
|
};
|
||||||
|
|
||||||
|
return Redirect::to(&login_url).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);
|
||||||
|
|
||||||
|
if let Some(ref next) = creds.next {
|
||||||
|
Redirect::to(next)
|
||||||
|
} else {
|
||||||
|
Redirect::to("/")
|
||||||
|
}
|
||||||
|
.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(),
|
||||||
|
}
|
||||||
|
}
|
21
src/lib.rs
21
src/lib.rs
@ -17,14 +17,18 @@ macro_rules! testdb {
|
|||||||
i18n!("locales", fallback = "de-AT");
|
i18n!("locales", fallback = "de-AT");
|
||||||
|
|
||||||
use admin::station::Station;
|
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 partials::page;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::net::TcpListener;
|
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;
|
pub(crate) mod admin;
|
||||||
|
mod auth;
|
||||||
pub(crate) mod models;
|
pub(crate) mod models;
|
||||||
mod partials;
|
mod partials;
|
||||||
pub(crate) mod station;
|
pub(crate) mod station;
|
||||||
@ -128,21 +132,28 @@ impl FromRef<AppState> for Arc<SqlitePool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn router(db: SqlitePool) -> Router {
|
fn router(db: SqlitePool) -> Router {
|
||||||
let session_store = MemoryStore::default();
|
let session_store = SqliteStore::new(db.clone());
|
||||||
let session_layer = SessionManagerLayer::new(session_store);
|
|
||||||
|
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) };
|
let state = AppState { db: Arc::new(db) };
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.nest("/s/{id}/{code}", station::routes()) // TODO: maybe switch to "/"
|
.nest("/s/{id}/{code}", station::routes()) // TODO: maybe switch to "/"
|
||||||
.nest("/admin", admin::routes())
|
.nest("/admin", admin::routes())
|
||||||
|
.nest("/auth", auth::routes())
|
||||||
.route("/pico.css", get(serve_pico_css))
|
.route("/pico.css", get(serve_pico_css))
|
||||||
.route("/style.css", get(serve_my_css))
|
.route("/style.css", get(serve_my_css))
|
||||||
.route("/leaflet.css", get(serve_leaflet_css))
|
.route("/leaflet.css", get(serve_leaflet_css))
|
||||||
.route("/leaflet.js", get(serve_leaflet_js))
|
.route("/leaflet.js", get(serve_leaflet_js))
|
||||||
.route("/marker.png", get(serve_marker_png))
|
.route("/marker.png", get(serve_marker_png))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
.layer(session_layer)
|
.layer(auth_layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts the main application.
|
/// Starts the main application.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user