Yasir Arefin Tusher
Yasir Arefin Tusher

Reputation: 352

Flutter Web WebSocket Connection to Azure SignalR Hub: Passing Access Token After Negotiate

I'm wrestling with a SignalR authentication puzzle that's turning my hair gray
Scenario:

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:

Each attempt cannot connected with the authenticated user

Specific Constraints:

Upvotes: 1

Views: 94

Answers (1)

Sampath
Sampath

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),
      ),
    );
  }
}

Out in flutter

In general, you connect to wss://xxxx.azurewebsites.net/chat-hub/negotiate and use https://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

Related Questions