fix: pass JWT token in WebSocket connection headers

WebSocket connections to /ws/agent were rejected by Kong (401)
because the Authorization header was not included. Now reads
access_token from secure storage and passes it in the WebSocket
upgrade request headers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-23 16:43:31 -08:00
parent 9cdc4933dc
commit 803cea0fe4
3 changed files with 15 additions and 4 deletions

View File

@ -31,7 +31,10 @@ class WebSocketClient {
final uri = Uri.parse('$baseUrl$path'); final uri = Uri.parse('$baseUrl$path');
try { try {
_channel = WebSocketChannel.connect(uri); _channel = WebSocketChannel.connect(
uri,
headers: token != null ? {'Authorization': 'Bearer $token'} : null,
);
_isConnected = true; _isConnected = true;
_reconnectAttempts = 0; _reconnectAttempts = 0;

View File

@ -11,14 +11,17 @@ class ChatRepositoryImpl implements ChatRepository {
final ChatRemoteDatasource _remoteDatasource; final ChatRemoteDatasource _remoteDatasource;
final ChatLocalDatasource _localDatasource; final ChatLocalDatasource _localDatasource;
final WebSocketClient _webSocketClient; final WebSocketClient _webSocketClient;
final Future<String?> Function() _getAccessToken;
ChatRepositoryImpl({ ChatRepositoryImpl({
required ChatRemoteDatasource remoteDatasource, required ChatRemoteDatasource remoteDatasource,
required ChatLocalDatasource localDatasource, required ChatLocalDatasource localDatasource,
required WebSocketClient webSocketClient, required WebSocketClient webSocketClient,
required Future<String?> Function() getAccessToken,
}) : _remoteDatasource = remoteDatasource, }) : _remoteDatasource = remoteDatasource,
_localDatasource = localDatasource, _localDatasource = localDatasource,
_webSocketClient = webSocketClient; _webSocketClient = webSocketClient,
_getAccessToken = getAccessToken;
@override @override
Stream<StreamEvent> sendMessage({ Stream<StreamEvent> sendMessage({
@ -39,7 +42,8 @@ class ChatRepositoryImpl implements ChatRepository {
final taskId = response['taskId'] as String? ?? response['task_id'] as String?; final taskId = response['taskId'] as String? ?? response['task_id'] as String?;
// Connect to the agent WebSocket and subscribe to the session // Connect to the agent WebSocket and subscribe to the session
await _webSocketClient.connect('/ws/agent'); final token = await _getAccessToken();
await _webSocketClient.connect('/ws/agent', token: token);
_webSocketClient.send({ _webSocketClient.send({
'event': 'subscribe_session', 'event': 'subscribe_session',
'data': {'sessionId': returnedSessionId, 'taskId': taskId}, 'data': {'sessionId': returnedSessionId, 'taskId': taskId},
@ -87,7 +91,8 @@ class ChatRepositoryImpl implements ChatRepository {
sessionId; sessionId;
final taskId = response['taskId'] as String? ?? response['task_id'] as String?; final taskId = response['taskId'] as String? ?? response['task_id'] as String?;
await _webSocketClient.connect('/ws/agent'); final voiceToken = await _getAccessToken();
await _webSocketClient.connect('/ws/agent', token: voiceToken);
_webSocketClient.send({ _webSocketClient.send({
'event': 'subscribe_session', 'event': 'subscribe_session',
'data': {'sessionId': returnedSessionId, 'taskId': taskId}, 'data': {'sessionId': returnedSessionId, 'taskId': taskId},

View File

@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../../../../core/network/dio_client.dart'; import '../../../../core/network/dio_client.dart';
import '../../../../core/network/websocket_client.dart'; import '../../../../core/network/websocket_client.dart';
import '../../../auth/data/providers/auth_provider.dart';
import '../../data/datasources/chat_local_datasource.dart'; import '../../data/datasources/chat_local_datasource.dart';
import '../../data/datasources/chat_remote_datasource.dart'; import '../../data/datasources/chat_remote_datasource.dart';
import '../../data/models/chat_message_model.dart'; import '../../data/models/chat_message_model.dart';
@ -39,12 +40,14 @@ final chatRepositoryProvider = Provider<ChatRepository>((ref) {
final remote = ref.watch(chatRemoteDatasourceProvider); final remote = ref.watch(chatRemoteDatasourceProvider);
final local = ref.watch(chatLocalDatasourceProvider); final local = ref.watch(chatLocalDatasourceProvider);
final ws = ref.watch(webSocketClientProvider); final ws = ref.watch(webSocketClientProvider);
final storage = ref.watch(secureStorageProvider);
// Use a no-op local datasource if SharedPreferences is not yet ready // Use a no-op local datasource if SharedPreferences is not yet ready
return ChatRepositoryImpl( return ChatRepositoryImpl(
remoteDatasource: remote, remoteDatasource: remote,
localDatasource: local ?? _NoOpLocalDatasource(), localDatasource: local ?? _NoOpLocalDatasource(),
webSocketClient: ws, webSocketClient: ws,
getAccessToken: () => storage.read(key: 'access_token'),
); );
}); });