fix(chat): ensure WebSocket auth token from query or cookie
- Chat server: accept token from ?token= or access_token cookie (httpOnly) - Frontend: append token to WS URL when available (TokenStorage)
This commit is contained in:
parent
98f6db3a1d
commit
a53dc358e6
4 changed files with 42 additions and 12 deletions
|
|
@ -29,6 +29,7 @@ interface WebSocketService {
|
|||
}
|
||||
|
||||
import { logger } from '@/utils/logger';
|
||||
import { TokenStorage } from '@/services/tokenStorage';
|
||||
|
||||
class WebSocketServiceImpl implements WebSocketService {
|
||||
private ws: WebSocket | null = null;
|
||||
|
|
@ -45,26 +46,26 @@ class WebSocketServiceImpl implements WebSocketService {
|
|||
return;
|
||||
}
|
||||
|
||||
// CORRECTION: Le serveur Rust expose le WebSocket sur /ws
|
||||
const wsUrl = (() => {
|
||||
// CORRECTION: Le serveur Rust expose le WebSocket sur /ws et requiert token en query (v0.101)
|
||||
const baseUrl = (() => {
|
||||
const url = import.meta.env.VITE_WS_URL;
|
||||
if (!url) {
|
||||
if (import.meta.env.PROD) {
|
||||
throw new Error('VITE_WS_URL must be defined in production');
|
||||
}
|
||||
// Fallback uniquement en développement - utiliser chemin relatif par défaut
|
||||
// SECURITY: Ne jamais utiliser localhost hardcodé, utiliser chemin relatif
|
||||
// Convertir chemin relatif en URL WebSocket
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return `${protocol}//${window.location.host}/ws`;
|
||||
}
|
||||
// Convertir URL relative en URL absolue pour WebSocket
|
||||
if (url.startsWith('/')) {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return `${protocol}//${window.location.host}${url}`;
|
||||
}
|
||||
return url;
|
||||
})();
|
||||
const token = TokenStorage.getAccessToken();
|
||||
const wsUrl = token
|
||||
? `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}`
|
||||
: baseUrl;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,13 @@ REDIS_ENABLE=true
|
|||
RABBITMQ_URL=amqp://veza:password@localhost:5672/%2f
|
||||
RABBITMQ_ENABLE=true
|
||||
|
||||
# ClamAV (v0.101 - virus scanning for uploads)
|
||||
# Dev (Docker): ENABLE_CLAMAV=true, CLAMAV_REQUIRED=false (accepts uploads if ClamAV slow/down)
|
||||
# Prod: CLAMAV_REQUIRED=true (startup fails if ClamAV unavailable)
|
||||
ENABLE_CLAMAV=true
|
||||
CLAMAV_REQUIRED=false
|
||||
CLAMAV_ADDRESS=clamav:3310
|
||||
|
||||
# Security
|
||||
JWT_SECRET=your-secret-key-here-change-in-production
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:3000
|
||||
|
|
|
|||
|
|
@ -322,9 +322,10 @@ async fn main() -> Result<(), ChatError> {
|
|||
let ws_state_clone = ws_state.clone();
|
||||
move |ws: WebSocketUpgrade,
|
||||
query: Query<HashMap<String, String>>,
|
||||
headers: axum::http::HeaderMap,
|
||||
request_id: Option<Extension<Uuid>>|
|
||||
async move {
|
||||
websocket_handler(ws, query, State(ws_state_clone), request_id).await
|
||||
websocket_handler(ws, query, headers, State(ws_state_clone), request_id).await
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
use axum::extract::ws::{Message, WebSocket};
|
||||
use axum::extract::{Query, State, WebSocketUpgrade};
|
||||
use axum::http::HeaderMap;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use futures_util::StreamExt;
|
||||
|
|
@ -45,6 +46,27 @@ pub struct WebSocketState {
|
|||
pub rate_limiter: Arc<crate::security::RateLimiter>,
|
||||
}
|
||||
|
||||
/// Extract access token from query param (?token=) or Cookie header (access_token)
|
||||
fn extract_token(params: &HashMap<String, String>, headers: &HeaderMap) -> Option<String> {
|
||||
if let Some(t) = params.get("token") {
|
||||
return Some(t.clone());
|
||||
}
|
||||
if let Some(cookie_header) = headers.get(axum::http::header::COOKIE) {
|
||||
if let Ok(cookie_str) = cookie_header.to_str() {
|
||||
for part in cookie_str.split(';') {
|
||||
let part = part.trim();
|
||||
if part.starts_with("access_token=") {
|
||||
let value = part.trim_start_matches("access_token=").trim();
|
||||
if !value.is_empty() {
|
||||
return Some(value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Handler principal pour les connexions WebSocket
|
||||
///
|
||||
/// Cette fonction gère la mise à niveau de la connexion HTTP vers WebSocket
|
||||
|
|
@ -53,8 +75,8 @@ pub struct WebSocketState {
|
|||
pub async fn websocket_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
headers: HeaderMap,
|
||||
State(state): State<WebSocketState>,
|
||||
// FIX #13: Extraire le request_id depuis les extensions (ajouté par request_id_middleware)
|
||||
request_id: Option<axum::extract::Extension<Uuid>>,
|
||||
) -> Response {
|
||||
// FIX #13: Extraire le request_id pour la corrélation
|
||||
|
|
@ -69,16 +91,15 @@ pub async fn websocket_handler(
|
|||
async move {
|
||||
info!(request_id = %request_id, "🔌 Nouvelle connexion WebSocket demandée");
|
||||
|
||||
let token = match params.get("token") {
|
||||
let token = match extract_token(¶ms, &headers) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
error!(request_id = %request_id, "❌ Token manquant");
|
||||
error!(request_id = %request_id, "❌ Token manquant (query ?token= ou cookie access_token)");
|
||||
return (StatusCode::UNAUTHORIZED, "Missing token").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// Validate token
|
||||
match state.jwt_manager.validate_access_token(token).await {
|
||||
match state.jwt_manager.validate_access_token(&token).await {
|
||||
Ok(claims) => {
|
||||
info!(
|
||||
request_id = %request_id,
|
||||
|
|
|
|||
Loading…
Reference in a new issue