Security: Require authentication for all WebSocket and API endpoints
- REST API: All session/code/machine endpoints now require AuthenticatedUser - Viewer WebSocket: Requires JWT token in query params (token=...) - Agent WebSocket: Requires either valid support code OR API key - Dashboard: Passes JWT token when connecting to viewer WS - Native viewer: Passes token in protocol URL and WebSocket connection - Added AGENT_API_KEY env var support for persistent agents - Added get_status() to SupportCodeManager for auth validation This fixes the security vulnerability where unauthenticated agents could connect and appear in the dashboard without any credentials. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ use axum::{
|
||||
Query, State,
|
||||
},
|
||||
response::IntoResponse,
|
||||
http::StatusCode,
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use prost::Message as ProstMessage;
|
||||
@@ -30,6 +31,9 @@ pub struct AgentParams {
|
||||
support_code: Option<String>,
|
||||
#[serde(default)]
|
||||
hostname: Option<String>,
|
||||
/// API key for persistent (managed) agents
|
||||
#[serde(default)]
|
||||
api_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -37,6 +41,9 @@ pub struct ViewerParams {
|
||||
session_id: String,
|
||||
#[serde(default = "default_viewer_name")]
|
||||
viewer_name: String,
|
||||
/// JWT token for authentication (required)
|
||||
#[serde(default)]
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
fn default_viewer_name() -> String {
|
||||
@@ -48,15 +55,71 @@ pub async fn agent_ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<AgentParams>,
|
||||
) -> impl IntoResponse {
|
||||
let agent_id = params.agent_id;
|
||||
let agent_name = params.hostname.or(params.agent_name).unwrap_or_else(|| agent_id.clone());
|
||||
let support_code = params.support_code;
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let agent_id = params.agent_id.clone();
|
||||
let agent_name = params.hostname.clone().or(params.agent_name.clone()).unwrap_or_else(|| agent_id.clone());
|
||||
let support_code = params.support_code.clone();
|
||||
let api_key = params.api_key.clone();
|
||||
|
||||
// SECURITY: Agent must provide either a support code OR an API key
|
||||
// Support code = ad-hoc support session (technician generated code)
|
||||
// API key = persistent managed agent
|
||||
|
||||
if support_code.is_none() && api_key.is_none() {
|
||||
warn!("Agent connection rejected: {} - no support code or API key", agent_id);
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// Validate support code if provided
|
||||
if let Some(ref code) = support_code {
|
||||
// Check if it's a valid, pending support code
|
||||
let code_info = state.support_codes.get_status(code).await;
|
||||
if code_info.is_none() {
|
||||
warn!("Agent connection rejected: {} - invalid support code {}", agent_id, code);
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
let status = code_info.unwrap();
|
||||
if status != "pending" && status != "connected" {
|
||||
warn!("Agent connection rejected: {} - support code {} has status {}", agent_id, code, status);
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
info!("Agent {} authenticated via support code {}", agent_id, code);
|
||||
}
|
||||
|
||||
// Validate API key if provided (for persistent agents)
|
||||
if let Some(ref key) = api_key {
|
||||
// For now, we'll accept API keys that match the JWT secret or a configured agent key
|
||||
// In production, this should validate against a database of registered agents
|
||||
if !validate_agent_api_key(&state, key).await {
|
||||
warn!("Agent connection rejected: {} - invalid API key", agent_id);
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
info!("Agent {} authenticated via API key", agent_id);
|
||||
}
|
||||
|
||||
let sessions = state.sessions.clone();
|
||||
let support_codes = state.support_codes.clone();
|
||||
let db = state.db.clone();
|
||||
|
||||
ws.on_upgrade(move |socket| handle_agent_connection(socket, sessions, support_codes, db, agent_id, agent_name, support_code))
|
||||
Ok(ws.on_upgrade(move |socket| handle_agent_connection(socket, sessions, support_codes, db, agent_id, agent_name, support_code)))
|
||||
}
|
||||
|
||||
/// Validate an agent API key
|
||||
async fn validate_agent_api_key(state: &AppState, api_key: &str) -> bool {
|
||||
// Check if API key is a valid JWT (allows using dashboard token for testing)
|
||||
if state.jwt_config.validate_token(api_key).is_ok() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check against configured agent API key if set
|
||||
if let Some(ref configured_key) = state.agent_api_key {
|
||||
if api_key == configured_key {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// In future: validate against database of registered agents
|
||||
false
|
||||
}
|
||||
|
||||
/// WebSocket handler for viewer connections
|
||||
@@ -64,13 +127,27 @@ pub async fn viewer_ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<ViewerParams>,
|
||||
) -> impl IntoResponse {
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
// Require JWT token for viewers
|
||||
let token = params.token.ok_or_else(|| {
|
||||
warn!("Viewer connection rejected: missing token");
|
||||
StatusCode::UNAUTHORIZED
|
||||
})?;
|
||||
|
||||
// Validate the token
|
||||
let claims = state.jwt_config.validate_token(&token).map_err(|e| {
|
||||
warn!("Viewer connection rejected: invalid token: {}", e);
|
||||
StatusCode::UNAUTHORIZED
|
||||
})?;
|
||||
|
||||
info!("Viewer {} authenticated via JWT", claims.username);
|
||||
|
||||
let session_id = params.session_id;
|
||||
let viewer_name = params.viewer_name;
|
||||
let sessions = state.sessions.clone();
|
||||
let db = state.db.clone();
|
||||
|
||||
ws.on_upgrade(move |socket| handle_viewer_connection(socket, sessions, db, session_id, viewer_name))
|
||||
Ok(ws.on_upgrade(move |socket| handle_viewer_connection(socket, sessions, db, session_id, viewer_name)))
|
||||
}
|
||||
|
||||
/// Handle an agent WebSocket connection
|
||||
|
||||
Reference in New Issue
Block a user