diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 08a10e2..100a9a8 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -53,6 +53,7 @@ services: FURUMI_PLAYER_DATABASE_URL: "postgres://${POSTGRES_USER:-furumi}:${POSTGRES_PASSWORD:-furumi}@db:5432/${POSTGRES_DB:-furumi}" FURUMI_PLAYER_STORAGE_DIR: "/storage" FURUMI_PLAYER_BIND: "0.0.0.0:8085" + FURUMI_PLAYER_API_KEY: "node-player-api-key" volumes: - ./storage:/storage restart: always diff --git a/furumi-web-player/src/main.rs b/furumi-web-player/src/main.rs index 66696dc..b8a8592 100644 --- a/furumi-web-player/src/main.rs +++ b/furumi-web-player/src/main.rs @@ -39,6 +39,10 @@ struct Args { /// OIDC Session Secret (32+ chars, for HMAC). Random if not provided. #[arg(long, env = "FURUMI_PLAYER_OIDC_SESSION_SECRET")] oidc_session_secret: Option, + + /// API key for x-api-key header auth (alternative to OIDC session) + #[arg(long, env = "FURUMI_PLAYER_API_KEY")] + api_key: Option, } #[tokio::main] @@ -90,10 +94,15 @@ async fn main() -> Result<(), Box> { std::process::exit(1); }); + if args.api_key.is_some() { + tracing::info!("x-api-key auth: enabled"); + } + let state = Arc::new(web::AppState { pool, storage_dir: Arc::new(args.storage_dir), oidc: oidc_state, + api_key: args.api_key, }); tracing::info!("Web player: http://{}", bind_addr); diff --git a/furumi-web-player/src/web/auth.rs b/furumi-web-player/src/web/auth.rs index c2f626c..33f8184 100644 --- a/furumi-web-player/src/web/auth.rs +++ b/furumi-web-player/src/web/auth.rs @@ -5,6 +5,8 @@ use axum::{ middleware::Next, response::{Html, IntoResponse, Redirect, Response}, }; + +const X_API_KEY: &str = "x-api-key"; use openidconnect::{ core::{CoreClient, CoreProviderMetadata, CoreResponseType}, reqwest::async_http_client, @@ -92,37 +94,51 @@ fn verify_sso_cookie(secret: &[u8], cookie_val: &str) -> Option { } } -/// Auth middleware: requires valid SSO session cookie. +/// Auth middleware: requires valid SSO session cookie or x-api-key header. pub async fn require_auth( State(state): State>, req: Request, next: Next, ) -> Response { - let oidc = match &state.oidc { - Some(o) => o, - None => return next.run(req).await, // No OIDC configured = no auth - }; - - let cookies = req - .headers() - .get(header::COOKIE) - .and_then(|v| v.to_str().ok()) - .unwrap_or(""); - - for c in cookies.split(';') { - let c = c.trim(); - if let Some(val) = c.strip_prefix(&format!("{}=", SESSION_COOKIE)) { - if verify_sso_cookie(&oidc.session_secret, val).is_some() { + // 1. Check x-api-key header (if configured) + if let Some(ref expected) = state.api_key { + if let Some(val) = req + .headers() + .get(X_API_KEY) + .and_then(|v| v.to_str().ok()) + { + if val == expected { return next.run(req).await; } } } + // 2. Check SSO session cookie (if OIDC configured) + if let Some(ref oidc) = state.oidc { + let cookies = req + .headers() + .get(header::COOKIE) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + for c in cookies.split(';') { + let c = c.trim(); + if let Some(val) = c.strip_prefix(&format!("{}=", SESSION_COOKIE)) { + if verify_sso_cookie(&oidc.session_secret, val).is_some() { + return next.run(req).await; + } + } + } + } + let uri = req.uri().to_string(); if uri.starts_with("/api/") { (StatusCode::UNAUTHORIZED, "Unauthorized").into_response() - } else { + } else if state.oidc.is_some() { Redirect::to("/login").into_response() + } else { + // Only API key configured — no web login available + (StatusCode::UNAUTHORIZED, "Unauthorized").into_response() } } diff --git a/furumi-web-player/src/web/mod.rs b/furumi-web-player/src/web/mod.rs index e53ff2c..90d761c 100644 --- a/furumi-web-player/src/web/mod.rs +++ b/furumi-web-player/src/web/mod.rs @@ -13,6 +13,7 @@ pub struct AppState { #[allow(dead_code)] pub storage_dir: Arc, pub oidc: Option>, + pub api_key: Option, } pub fn build_router(state: Arc) -> Router { @@ -32,9 +33,9 @@ pub fn build_router(state: Arc) -> Router { .route("/", get(player_html)) .nest("/api", library); - let has_oidc = state.oidc.is_some(); + let requires_auth = state.oidc.is_some(); - let app = if has_oidc { + let app = if requires_auth { authed .route_layer(middleware::from_fn_with_state(state.clone(), auth::require_auth)) } else {