feat: added auth by api key
Some checks failed
Publish Metadata Agent Image / build-and-push-image (push) Successful in 1m15s
Publish Web Player Image / build-and-push-image (push) Successful in 1m5s
Publish Server Image / build-and-push-image (push) Failing after 12m24s

This commit is contained in:
Boris Cherepanov
2026-03-23 12:07:57 +03:00
parent c30a3aff5d
commit 03f95cfd05
4 changed files with 46 additions and 19 deletions

View File

@@ -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

View File

@@ -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<String>,
/// API key for x-api-key header auth (alternative to OIDC session)
#[arg(long, env = "FURUMI_PLAYER_API_KEY")]
api_key: Option<String>,
}
#[tokio::main]
@@ -90,10 +94,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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);

View File

@@ -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<String> {
}
}
/// 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<Arc<AppState>>,
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()
}
}

View File

@@ -13,6 +13,7 @@ pub struct AppState {
#[allow(dead_code)]
pub storage_dir: Arc<PathBuf>,
pub oidc: Option<Arc<auth::OidcState>>,
pub api_key: Option<String>,
}
pub fn build_router(state: Arc<AppState>) -> Router {
@@ -32,9 +33,9 @@ pub fn build_router(state: Arc<AppState>) -> 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 {