feat: add separate auth middleware for web routes

main
Jef Roosens 2025-03-30 09:50:55 +02:00
parent b3e49be299
commit 3071685950
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
11 changed files with 89 additions and 19 deletions

View File

@ -8,7 +8,7 @@ use axum::{
use crate::server::{ use crate::server::{
error::{AppError, AppResult}, error::{AppError, AppResult},
gpodder::{ gpodder::{
auth_middleware, auth_api_middleware,
format::{Format, StringWithFormat}, format::{Format, StringWithFormat},
models, models,
}, },
@ -19,7 +19,7 @@ pub fn router(ctx: Context) -> Router<Context> {
Router::new() Router::new()
.route("/{username}", get(get_devices)) .route("/{username}", get(get_devices))
.route("/{username}/{id}", post(post_device)) .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( async fn get_devices(

View File

@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use crate::server::{ use crate::server::{
error::{AppError, AppResult}, error::{AppError, AppResult},
gpodder::{ gpodder::{
auth_middleware, auth_api_middleware,
format::{Format, StringWithFormat}, format::{Format, StringWithFormat},
models, models,
models::UpdatedUrlsResponse, models::UpdatedUrlsResponse,
@ -24,7 +24,7 @@ pub fn router(ctx: Context) -> Router<Context> {
"/{username}", "/{username}",
post(post_episode_actions).get(get_episode_actions), 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( async fn post_episode_actions(

View File

@ -9,7 +9,7 @@ use serde::Deserialize;
use crate::server::{ use crate::server::{
error::{AppError, AppResult}, error::{AppError, AppResult},
gpodder::{ gpodder::{
auth_middleware, auth_api_middleware,
format::{Format, StringWithFormat}, format::{Format, StringWithFormat},
models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse}, models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
}, },
@ -22,7 +22,7 @@ pub fn router(ctx: Context) -> Router<Context> {
"/{username}/{id}", "/{username}/{id}",
post(post_subscription_changes).get(get_subscription_changes), 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( pub async fn post_subscription_changes(

View File

@ -8,7 +8,7 @@ use axum::{
use crate::server::{ use crate::server::{
error::{AppError, AppResult}, error::{AppError, AppResult},
gpodder::{ gpodder::{
auth_middleware, auth_api_middleware,
format::{Format, StringWithFormat}, format::{Format, StringWithFormat},
models::{SyncStatus, SyncStatusDelta}, models::{SyncStatus, SyncStatusDelta},
}, },
@ -21,7 +21,7 @@ pub fn router(ctx: Context) -> Router<Context> {
"/{username}", "/{username}",
get(get_sync_status).post(post_sync_status_changes), 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( pub async fn get_sync_status(

View File

@ -36,8 +36,13 @@ pub fn router(ctx: Context) -> Router<Context> {
)) ))
} }
/// This middleware accepts /// Middleware that can authenticate both with session cookies and basic auth. If basic auth is
pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next: Next) -> Response { /// used, no session is created. If authentication fails, the server returns a 401.
pub async fn auth_api_middleware(
State(ctx): State<Context>,
mut req: Request,
next: Next,
) -> Response {
// SAFETY: this extractor's error type is Infallible // SAFETY: this extractor's error type is Infallible
let mut jar: CookieJar = req.extract_parts().await.unwrap(); let mut jar: CookieJar = req.extract_parts().await.unwrap();
let mut auth_user = None; let mut auth_user = None;

View File

@ -7,7 +7,7 @@ use axum::{
use crate::server::{ use crate::server::{
error::{AppError, AppResult}, error::{AppError, AppResult},
gpodder::{auth_middleware, format::StringWithFormat}, gpodder::{auth_api_middleware, format::StringWithFormat},
Context, Context,
}; };
@ -18,7 +18,7 @@ pub fn router(ctx: Context) -> Router<Context> {
get(get_device_subscriptions).put(put_device_subscriptions), get(get_device_subscriptions).put(put_device_subscriptions),
) )
.route("/{username}", get(get_user_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( pub async fn get_device_subscriptions(

View File

@ -10,7 +10,8 @@ use axum::{
extract::Request, extract::Request,
http::StatusCode, http::StatusCode,
middleware::Next, middleware::Next,
response::{IntoResponse, Response}, response::{IntoResponse, Redirect, Response},
routing::get,
Router, Router,
}; };
use http_body_util::BodyExt; use http_body_util::BodyExt;
@ -27,6 +28,7 @@ pub fn app(ctx: Context) -> Router {
.merge(gpodder::router(ctx.clone())) .merge(gpodder::router(ctx.clone()))
.nest("/static", r#static::router()) .nest("/static", r#static::router())
.nest("/_", web::router(ctx.clone())) .nest("/_", web::router(ctx.clone()))
.route("/", get(|| async { Redirect::to("/_") }))
.layer(axum::middleware::from_fn(header_logger)) .layer(axum::middleware::from_fn(header_logger))
.layer(axum::middleware::from_fn(body_logger)) .layer(axum::middleware::from_fn(body_logger))
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())

View File

@ -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 crate::web::{Page, TemplateExt, TemplateResponse, View};
use super::Context; use super::{error::AppError, Context};
pub fn router(_ctx: Context) -> Router<Context> { const SESSION_ID_COOKIE: &str = "sessionid";
Router::new().route("/", get(get_index))
pub fn router(ctx: Context) -> Router<Context> {
Router::new()
.route("/", get(get_index))
.layer(middleware::from_fn_with_state(
ctx.clone(),
auth_web_middleware,
))
} }
async fn get_index(State(ctx): State<Context>, headers: HeaderMap) -> TemplateResponse<Page<View>> { async fn get_index(State(ctx): State<Context>, headers: HeaderMap) -> TemplateResponse<Page<View>> {
View::Index.page(&headers).response(&ctx.tera) 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<Context>,
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::<i64>().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()
}
}

View File

@ -71,7 +71,10 @@ pub fn initialize_tera() -> tera::Result<tera::Tera> {
tera.add_raw_templates([ tera.add_raw_templates([
(BASE_TEMPLATE, include_str!("templates/base.html")), (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) Ok(tera)

View File

@ -7,7 +7,7 @@ pub enum View {
impl Template for View { impl Template for View {
fn template(&self) -> &'static str { fn template(&self) -> &'static str {
match self { match self {
Self::Index => "index.html", Self::Index => "views/index.html",
} }
} }