Reputation: 352
I'm wrestling with a SignalR authentication puzzle that's turning my hair gray
Scenario:
I have a SignalR service hosted on Azure
Previously worked fine with web_socket_channel
in Flutter web when there was no authentication
Server recently added user authentication to the hub for map specific user
Mobile: Successfully passes access token via SignalR client's access token factory
Web: Using web_socket_channel
package, stuck on token transmission
The curl of the negotiation step:
curl --location --request POST 'http://example.com/chat-hub/negotiate?negotiateVersion=1' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer $accessToken'
Negotiate Response Looks Like:
{
"negotiateVersion": 1,
"connectionId": "hpCRJgUbCr7Y8x5UDCzhRA",
"connectionToken": "OAtaVE1oEv4z0hceHAKhCg",
"availableTransports": [
{
"transport": "WebSockets",
"transferFormats": [
"Text",
"Binary"
]
},
{
"transport": "ServerSentEvents",
"transferFormats": [
"Text"
]
},
{
"transport": "LongPolling",
"transferFormats": [
"Text",
"Binary"
]
}
]
}
The Burning Question: How do I inject the authentication access-token/connection-token when establishing a WebSocket connection for this SignalR endpoint on Flutter Web?
I've tried:
Appending accessToken to URL: ws://ctifiloops.bjitgroup.com:8088/chat-hub?access-token={{accessToken}}
Appending connectionToken to URL: ws://ctifiloops.bjitgroup.com:8088/chat-hub?id={{conncetionToken}}
Each attempt cannot connected with the authenticated user
Specific Constraints:
Using web_socket_channel
Azure SignalR Hub
Flutter Web targeting
Must authenticate per-connection
Access token available from login
Upvotes: 1
Views: 94
Reputation: 3591
The Code below is to Establish a WebSocket Connection to an Azure SignalR Hub in Flutter Web:
Make sure you enable WebSockets in your app service configurations.
Refer to this link for Authentication and Authorization in SignalR.
Refer to this link for details on how to communicate with WebSockets in Flutter.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
const title = 'SignalR WebSocket Demo';
return const MaterialApp(
title: title,
home: MyHomePage(title: title),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final TextEditingController _controller = TextEditingController();
WebSocketChannel? _channel;
bool _isConnected = false;
final String _signalRNegotiateUrl = "https://example.com/chat-hub/negotiate";
final String _accessToken = "your-access-token-here";
@override
void initState() {
super.initState();
_connectToSignalR();
}
Future<void> _connectToSignalR() async {
try {
final negotiateResponse = await _negotiate();
if (negotiateResponse != null) {
final connectionToken = negotiateResponse['connectionToken'];
final webSocketUrl = _buildWebSocketUrl(negotiateResponse['url'], connectionToken);
setState(() {
_channel = WebSocketChannel.connect(Uri.parse(webSocketUrl));
_isConnected = true;
});
_channel?.stream.listen(
(message) {
setState(() {
print("Received: $message");
});
},
onDone: () {
setState(() {
_isConnected = false;
});
},
onError: (error) {
setState(() {
_isConnected = false;
print("WebSocket error: $error");
});
},
);
}
} catch (e) {
print("Error connecting to SignalR: $e");
}
}
Future<Map<String, dynamic>?> _negotiate() async {
try {
final response = await http.post(
Uri.parse("$_signalRNegotiateUrl?negotiateVersion=1"),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $_accessToken',
},
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
print("Negotiate failed: ${response.statusCode}, ${response.body}");
return null;
}
} catch (e) {
print("Negotiate error: $e");
return null;
}
}
String _buildWebSocketUrl(String baseUrl, String connectionToken) {
final uri = Uri.parse(baseUrl);
return "${uri.replace(scheme: 'wss')}?id=$connectionToken";
}
void _sendMessage() {
if (_controller.text.isNotEmpty && _isConnected) {
_channel?.sink.add(_controller.text);
}
}
@override
void dispose() {
_channel?.sink.close(status.normalClosure);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Form(
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(labelText: 'Send a message'),
),
),
const SizedBox(height: 24),
_isConnected
? StreamBuilder(
stream: _channel?.stream,
builder: (context, snapshot) {
return Text(snapshot.hasData ? '${snapshot.data}' : 'No messages yet.');
},
)
: const Text('Not connected to WebSocket.'),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _sendMessage,
tooltip: 'Send message',
child: const Icon(Icons.send),
),
);
}
}
In general, you connect to
wss://xxxx.azurewebsites.net/chat-hub/negotiate
and usehttps://your-signalr-hub-url/chat-hub/negotiate?negotiateVersion=1
. For WebSocket transport, refer to this doc to configure it as follows:
final url = Uri.parse('https://your-signalr-hub-url/chat-hub/negotiate?negotiateVersion=1');
final response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $accessToken',
},
);
Upvotes: 0