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:
asepharyana
2026-08-27 22:04:32 +07:00
parent 7f64423615
commit 6db00b2266
7 changed files with 219 additions and 25 deletions
+27 -2
View File
@@ -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.