fix(api): perbaiki keamanan auth & WebSocket, tambah rate limiting
Security fixes hasil audit: - fix(auth): refresh token kini memakai claim typ=refresh; access token tidak bisa dipakai sebagai refresh token (sebelumnya bisa — eskalasi masa berlaku 1 jam -> 7 hari) - fix(api): layer JWT hanya melindungi route /sessions dan /chat; /auth/login, /auth/register, /auth/refresh, /health kini publik (sebelumnya semua route 401-lock, API tidak bisa dipakai sama sekali) - fix(ws): endpoint /ws kini memverifikasi token ZESDEX_WS_TOKEN via query param jika env diset (mencegah pemakaian LLM proxy terbuka) - feat(api): rate limiting login/register/refresh (20 request / 10 menit per client IP) memakai RateLimiter yang tadinya dead code - test(jwt): tambah unit test token type access vs refresh + expired
This commit is contained in:
@@ -2,15 +2,25 @@
|
||||
//!
|
||||
//! Enables web clients and other WS-capable consumers to connect
|
||||
//! and participate in sessions. Built on Axum's WebSocket support.
|
||||
//!
|
||||
//! # Security
|
||||
//!
|
||||
//! The WS endpoint accepts an optional `?token=` query parameter. When a
|
||||
//! `ZEESDEX_WS_TOKEN` env var is set, connections MUST present a matching
|
||||
//! token — otherwise the connection is rejected. This prevents the endpoint
|
||||
//! from being used as an open LLM proxy (anyone who can reach the port would
|
||||
//! otherwise run prompts at the server's API cost).
|
||||
|
||||
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
|
||||
use axum::extract::Query;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use futures_util::stream::StreamExt;
|
||||
use futures_util::SinkExt;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Shared application state for the WS server.
|
||||
pub struct WsState {
|
||||
@@ -18,6 +28,12 @@ pub struct WsState {
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Query parameters accepted on the `/ws` upgrade.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WsQuery {
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
/// Build the WebSocket router.
|
||||
pub fn build_router(state: Arc<WsState>) -> Router {
|
||||
Router::new()
|
||||
@@ -28,9 +44,18 @@ pub fn build_router(state: Arc<WsState>) -> Router {
|
||||
/// WebSocket upgrade handler.
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
Query(query): Query<WsQuery>,
|
||||
axum::extract::State(state): axum::extract::State<Arc<WsState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, state))
|
||||
let configured = std::env::var("ZESDEX_WS_TOKEN").ok().filter(|s| !s.is_empty());
|
||||
match configured {
|
||||
Some(expected) if query.token.as_deref() != Some(expected.as_str()) => {
|
||||
warn!("rejecting WS connection: missing/invalid token");
|
||||
// 401 Unauthorized — client did not present the required token.
|
||||
(axum::http::StatusCode::UNAUTHORIZED, "missing or invalid token").into_response()
|
||||
}
|
||||
_ => ws.on_upgrade(move |socket| handle_socket(socket, state)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an established WebSocket connection.
|
||||
|
||||
Reference in New Issue
Block a user