fix(auth): apply auth middleware to /me, /change-password, /logout and add credentials to admin.js

The protected auth routes (/me, /change-password, /logout) were merged
with public routes in auth_handler.rs but never had auth middleware
applied in main.rs — so the CurrentUserId extractor always failed with
401. Split auth_routes() into auth_public_routes() and
auth_protected_routes(), applying auth + CSRF middleware to the latter.

Also added credentials: 'same-origin' to all 13 fetch calls in admin.js
so the browser sends HttpOnly auth cookies with requests.
This commit is contained in:
Jared Wolff
2026-03-04 21:35:18 -05:00
parent 4293a30d50
commit 6db4e07538
3 changed files with 41 additions and 35 deletions
+10 -11
View File
@@ -16,25 +16,24 @@ use crate::interfaces::api::cookie_auth;
use crate::interfaces::errors::AppError;
use crate::interfaces::middleware::auth::CurrentUserId;
pub fn auth_routes() -> Router<Arc<AppState>> {
// Routes that do NOT require authentication
let public_routes = Router::new()
/// Public auth routes — no authentication required.
pub fn auth_public_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/status", get(get_system_status))
// OIDC endpoints (all public)
.route("/oidc/providers", get(oidc_providers))
.route("/oidc/authorize", get(oidc_authorize))
.route("/oidc/callback", get(oidc_callback))
.route("/oidc/exchange", post(oidc_exchange));
.route("/oidc/exchange", post(oidc_exchange))
}
// Routes that DO require authentication - we use route_layer to apply middleware
// The middleware will use the state passed with .with_state() from main.rs
let protected_routes = Router::new()
/// Protected auth routes — require authentication (auth + CSRF middleware
/// must be applied by the caller in main.rs).
pub fn auth_protected_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/me", get(get_current_user))
.route("/change-password", put(change_password))
.route("/logout", post(logout));
// Combine public and protected routes
public_routes.merge(protected_routes)
.route("/logout", post(logout))
}
/// Rate-limited auth routes — split out so main.rs can apply per-endpoint
+18 -11
View File
@@ -2,7 +2,6 @@
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -67,10 +66,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
if !storage_path.exists() {
std::fs::create_dir_all(&storage_path).expect("Failed to create storage directory");
}
let locales_path = PathBuf::from("./static/locales");
if !locales_path.exists() {
std::fs::create_dir_all(&locales_path).expect("Failed to create locales directory");
}
// Locales are embedded in the binary via rust-embed — no filesystem path needed.
// Initialize database pools if auth is enabled
let db_pools = if config.features.enable_auth {
@@ -94,7 +90,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
};
// Build all services via the factory
let factory = AppServiceFactory::with_config(storage_path, locales_path, config.clone());
let factory = AppServiceFactory::with_config(storage_path, None, config.clone());
let app_state = factory.build_app_state(db_pools).await
.expect("Failed to build application state. If running in Docker, ensure the storage volume is writable by the oxicloud user (UID 1001)");
@@ -171,7 +167,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
if config.features.enable_auth {
use interfaces::api::handlers::auth_handler::{
auth_routes, login_route, refresh_route, register_route, setup_route,
auth_protected_routes, auth_public_routes, login_route, refresh_route, register_route,
setup_route,
};
use oxicloud::interfaces::api::handlers::app_password_handler;
use oxicloud::interfaces::api::handlers::device_auth_handler;
@@ -227,8 +224,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
rate_limit_refresh,
))
.with_state(app_state.clone());
// Remaining auth routes (status, OIDC, protected /me, /logout, etc.)
let auth_router = auth_routes().with_state(app_state.clone());
// Public auth routes (status, OIDC)
let auth_public = auth_public_routes().with_state(app_state.clone());
// Protected auth routes (/me, /change-password, /logout) — require auth + CSRF
let auth_protected = auth_protected_routes()
.layer(axum::middleware::from_fn(csrf_middleware))
.layer(axum::middleware::from_fn_with_state(
app_state.clone(),
auth_middleware,
))
.with_state(app_state.clone());
// One-time setup route — public, rate-limited like register
let setup_router = setup_route()
.layer(axum::middleware::from_fn_with_state(
@@ -286,8 +291,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.nest("/api/auth", auth_login)
.nest("/api/auth", auth_register)
.nest("/api/auth", auth_refresh)
// Other auth endpoints (status, OIDC, protected /me, /logout)
.nest("/api/auth", auth_router)
// Public auth endpoints (status, OIDC)
.nest("/api/auth", auth_public)
// Protected auth endpoints (/me, /change-password, /logout)
.nest("/api/auth", auth_protected)
// One-time setup endpoint — public, rate-limited
.nest("/api", setup_router)
// Device Auth Grant public endpoints (authorize + token polling)