From 30716859507eaa73580f90d43a4aad56df937302 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 30 Mar 2025 09:50:55 +0200 Subject: [PATCH] feat: add separate auth middleware for web routes --- src/server/gpodder/advanced/devices.rs | 4 +- src/server/gpodder/advanced/episodes.rs | 4 +- src/server/gpodder/advanced/subscriptions.rs | 4 +- src/server/gpodder/advanced/sync.rs | 4 +- src/server/gpodder/mod.rs | 9 ++- src/server/gpodder/simple/subscriptions.rs | 4 +- src/server/mod.rs | 4 +- src/server/web/mod.rs | 68 ++++++++++++++++++-- src/web/mod.rs | 5 +- src/web/templates/{ => views}/index.html | 0 src/web/view.rs | 2 +- 11 files changed, 89 insertions(+), 19 deletions(-) rename src/web/templates/{ => views}/index.html (100%) diff --git a/src/server/gpodder/advanced/devices.rs b/src/server/gpodder/advanced/devices.rs index 67f4f79..9a23e38 100644 --- a/src/server/gpodder/advanced/devices.rs +++ b/src/server/gpodder/advanced/devices.rs @@ -8,7 +8,7 @@ use axum::{ use crate::server::{ error::{AppError, AppResult}, gpodder::{ - auth_middleware, + auth_api_middleware, format::{Format, StringWithFormat}, models, }, @@ -19,7 +19,7 @@ pub fn router(ctx: Context) -> Router { Router::new() .route("/{username}", get(get_devices)) .route("/{username}/{id}", post(post_device)) - .layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware)) + .layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware)) } async fn get_devices( diff --git a/src/server/gpodder/advanced/episodes.rs b/src/server/gpodder/advanced/episodes.rs index 5ea62d6..9a83cb4 100644 --- a/src/server/gpodder/advanced/episodes.rs +++ b/src/server/gpodder/advanced/episodes.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use crate::server::{ error::{AppError, AppResult}, gpodder::{ - auth_middleware, + auth_api_middleware, format::{Format, StringWithFormat}, models, models::UpdatedUrlsResponse, @@ -24,7 +24,7 @@ pub fn router(ctx: Context) -> Router { "/{username}", post(post_episode_actions).get(get_episode_actions), ) - .layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware)) + .layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware)) } async fn post_episode_actions( diff --git a/src/server/gpodder/advanced/subscriptions.rs b/src/server/gpodder/advanced/subscriptions.rs index b69f420..ad5d552 100644 --- a/src/server/gpodder/advanced/subscriptions.rs +++ b/src/server/gpodder/advanced/subscriptions.rs @@ -9,7 +9,7 @@ use serde::Deserialize; use crate::server::{ error::{AppError, AppResult}, gpodder::{ - auth_middleware, + auth_api_middleware, format::{Format, StringWithFormat}, models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse}, }, @@ -22,7 +22,7 @@ pub fn router(ctx: Context) -> Router { "/{username}/{id}", post(post_subscription_changes).get(get_subscription_changes), ) - .layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware)) + .layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware)) } pub async fn post_subscription_changes( diff --git a/src/server/gpodder/advanced/sync.rs b/src/server/gpodder/advanced/sync.rs index 728452c..d8ea4e1 100644 --- a/src/server/gpodder/advanced/sync.rs +++ b/src/server/gpodder/advanced/sync.rs @@ -8,7 +8,7 @@ use axum::{ use crate::server::{ error::{AppError, AppResult}, gpodder::{ - auth_middleware, + auth_api_middleware, format::{Format, StringWithFormat}, models::{SyncStatus, SyncStatusDelta}, }, @@ -21,7 +21,7 @@ pub fn router(ctx: Context) -> Router { "/{username}", get(get_sync_status).post(post_sync_status_changes), ) - .layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware)) + .layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware)) } pub async fn get_sync_status( diff --git a/src/server/gpodder/mod.rs b/src/server/gpodder/mod.rs index 83442f3..1bf8061 100644 --- a/src/server/gpodder/mod.rs +++ b/src/server/gpodder/mod.rs @@ -36,8 +36,13 @@ pub fn router(ctx: Context) -> Router { )) } -/// This middleware accepts -pub async fn auth_middleware(State(ctx): State, mut req: Request, next: Next) -> Response { +/// Middleware that can authenticate both with session cookies and basic auth. If basic auth is +/// used, no session is created. If authentication fails, the server returns a 401. +pub async fn auth_api_middleware( + State(ctx): State, + mut req: Request, + next: Next, +) -> Response { // SAFETY: this extractor's error type is Infallible let mut jar: CookieJar = req.extract_parts().await.unwrap(); let mut auth_user = None; diff --git a/src/server/gpodder/simple/subscriptions.rs b/src/server/gpodder/simple/subscriptions.rs index a91b766..016fb37 100644 --- a/src/server/gpodder/simple/subscriptions.rs +++ b/src/server/gpodder/simple/subscriptions.rs @@ -7,7 +7,7 @@ use axum::{ use crate::server::{ error::{AppError, AppResult}, - gpodder::{auth_middleware, format::StringWithFormat}, + gpodder::{auth_api_middleware, format::StringWithFormat}, Context, }; @@ -18,7 +18,7 @@ pub fn router(ctx: Context) -> Router { get(get_device_subscriptions).put(put_device_subscriptions), ) .route("/{username}", get(get_user_subscriptions)) - .layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware)) + .layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware)) } pub async fn get_device_subscriptions( diff --git a/src/server/mod.rs b/src/server/mod.rs index f9ef26a..c86dc2a 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -10,7 +10,8 @@ use axum::{ extract::Request, http::StatusCode, middleware::Next, - response::{IntoResponse, Response}, + response::{IntoResponse, Redirect, Response}, + routing::get, Router, }; use http_body_util::BodyExt; @@ -27,6 +28,7 @@ pub fn app(ctx: Context) -> Router { .merge(gpodder::router(ctx.clone())) .nest("/static", r#static::router()) .nest("/_", web::router(ctx.clone())) + .route("/", get(|| async { Redirect::to("/_") })) .layer(axum::middleware::from_fn(header_logger)) .layer(axum::middleware::from_fn(body_logger)) .layer(TraceLayer::new_for_http()) diff --git a/src/server/web/mod.rs b/src/server/web/mod.rs index f3c3e5d..f3a8691 100644 --- a/src/server/web/mod.rs +++ b/src/server/web/mod.rs @@ -1,13 +1,73 @@ -use axum::{extract::State, http::HeaderMap, routing::get, Router}; +use axum::{ + extract::{Request, State}, + http::HeaderMap, + middleware::{self, Next}, + response::{IntoResponse, Redirect, Response}, + routing::get, + RequestExt, Router, +}; +use axum_extra::extract::CookieJar; +use cookie::Cookie; use crate::web::{Page, TemplateExt, TemplateResponse, View}; -use super::Context; +use super::{error::AppError, Context}; -pub fn router(_ctx: Context) -> Router { - Router::new().route("/", get(get_index)) +const SESSION_ID_COOKIE: &str = "sessionid"; + +pub fn router(ctx: Context) -> Router { + Router::new() + .route("/", get(get_index)) + .layer(middleware::from_fn_with_state( + ctx.clone(), + auth_web_middleware, + )) } async fn get_index(State(ctx): State, headers: HeaderMap) -> TemplateResponse> { View::Index.page(&headers).response(&ctx.tera) } + +/// Middleware that authenticates the current user via the session token. If the credentials are +/// invalid, the user is redirected to the login page. +pub async fn auth_web_middleware( + State(ctx): State, + mut req: Request, + next: Next, +) -> Response { + // SAFETY: this extractor's error type is Infallible + let mut jar: CookieJar = req.extract_parts().await.unwrap(); + let redirect = Redirect::to("/_/login"); + + if let Some(session_id) = jar + .get(SESSION_ID_COOKIE) + .and_then(|c| c.value().parse::().ok()) + { + match tokio::task::spawn_blocking(move || { + let session = ctx.store.get_session(session_id)?; + ctx.store.refresh_session(&session)?; + + Ok(session) + }) + .await + .unwrap() + { + Ok(session) => { + req.extensions_mut().insert(session.user); + + (jar, next.run(req).await).into_response() + } + Err(gpodder::AuthErr::UnknownSession) => { + jar = jar.add( + Cookie::build((SESSION_ID_COOKIE, String::new())) + .max_age(cookie::time::Duration::ZERO), + ); + + (jar, redirect).into_response() + } + Err(err) => AppError::from(err).into_response(), + } + } else { + redirect.into_response() + } +} diff --git a/src/web/mod.rs b/src/web/mod.rs index df87f93..8b33c27 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -71,7 +71,10 @@ pub fn initialize_tera() -> tera::Result { tera.add_raw_templates([ (BASE_TEMPLATE, include_str!("templates/base.html")), - (View::Index.template(), include_str!("templates/index.html")), + ( + View::Index.template(), + include_str!("templates/views/index.html"), + ), ])?; Ok(tera) diff --git a/src/web/templates/index.html b/src/web/templates/views/index.html similarity index 100% rename from src/web/templates/index.html rename to src/web/templates/views/index.html diff --git a/src/web/view.rs b/src/web/view.rs index da51c86..55ca3ce 100644 --- a/src/web/view.rs +++ b/src/web/view.rs @@ -7,7 +7,7 @@ pub enum View { impl Template for View { fn template(&self) -> &'static str { match self { - Self::Index => "index.html", + Self::Index => "views/index.html", } }