# Use tokens (/en/realtime-media/voice/build/set-up-token-authentication/use-tokens/flutter)

> For AI agents: see the complete documentation index at [llms.txt](/llms.txt).

To protect your business, it is best practice to authenticate every client that joins a channel. This guide explains how to fetch an authentication token from your token server, use it to join a channel, and renew the token when it expires.

## Understand the tech

      
  
      
  
      
  
      
  
      
  
      
  
      
When a user attempts to connect to an Agora channel, your app retrieves a token from the token server in your security infrastructure. Your app then sends this token to Agora SDRTN® for authentication. Agora SDRTN® reads the information stored in the token to validate the request.

The following figure shows the call flow you implement to create step-up-authentication with Agora Voice Calling:

**Token authentication flow**

![token authentication flow](https://assets-docs.agora.io/images/video-sdk/token-authentication.svg)

## Prerequisites

Before starting, ensure that you have:

* Implemented the [Quickstart](../index.mdx) in your project.

* Deployed a token server using either of the following guides:

  * [Deploy a token server](deploy-token-server.mdx)
  * [Deploy a middleware server](middleware-token-server.md)

## Implement basic authentication

This section shows you how to implement basic authentication by acquiring a token and using it to join a channel.

### Use a token to join a channel

This section shows you how to integrate token authentication in your app.

1. Add the following to the `pubspec.yaml` file, under `dependencies`:

   ```yaml
   dependencies:
     # Agora Flutter SDK, use the latest version of agora_rtc_engine
     agora_rtc_engine: ^6.3.0
     # For making http requests
     http: ^0.13.5
   ```

2. Replace the contents in `/lib/main.dart` with the following code.

   **Sample code for basic authentication**

   ```dart
     import 'dart:convert';

       import 'package:agora_rtc_engine/agora_rtc_engine.dart';
       import 'package:flutter/material.dart';
       import 'package:http/http.dart' as http;

       void main() => runApp(const MyApp());

       /// This widget is the root of your application.
       class MyApp extends StatefulWidget {
       /// Construct the [MyApp]
       const MyApp({Key? key}) : super(key: key);

       @override
       State<MyApp> createState() => _MyAppState();
       }

       class _MyAppState extends State<MyApp> {

       @override
       Widget build(BuildContext context) {
         return MaterialApp(
         theme: ThemeData(
           primarySwatch: Colors.blue,
         ),
         home: Scaffold(
           appBar: AppBar(
             title: const Text('APIExample'),
           ),
           body: const JoinChannelVideoToken()),
         );
       }
       }

       class JoinChannelVideoToken extends StatefulWidget {
       const JoinChannelVideoToken({Key? key}) : super(key: key);

       @override
       State<StatefulWidget> createState() => _State();
       }

       class _State extends State<JoinChannelVideoToken> {
       late final RtcEngine _engine;
       bool _isReadyPreview = false;

       bool isJoined = false, switchCamera = true, switchRender = true;
       Set<int> remoteUid = {};
       static const String appId = '<Your app ID>'; // Fill in the information from Agora console
       static const String channelId = '<Your channel name>'; // Fill in the channel name
       static const String hostUrl = '<Your host URL and port>'; // Fill in the server URL and port

       @override
       void initState() {
         super.initState();
         _initEngine();
       }

       @override
       void dispose() {
         super.dispose();
         _dispose();
       }

       Future<void> _dispose() async {
         await _engine.leaveChannel();
         await _engine.release();
       }

       Future<void> _initEngine() async {
         _engine = createAgoraRtcEngine();
         await _engine.initialize(const RtcEngineContext(
         appId: appId,
         ));

         _engine.registerEventHandler(RtcEngineEventHandler(
         onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
           setState(() {
           isJoined = true;
           });
         },
         onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
           setState(() {
           remoteUid.add(rUid);
           });
         },
         onUserOffline:
           (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
           setState(() {
           remoteUid.removeWhere((element) => element == rUid);
           });
         },
         onLeaveChannel: (RtcConnection connection, RtcStats stats) {
           setState(() {
           isJoined = false;
           remoteUid.clear();
           });
         },
         onTokenPrivilegeWillExpire: (RtcConnection connection, String token) {
           _fetchToken(1234, channelId, 1, false);
         },
         onRequestToken: (RtcConnection connection) {
           _fetchToken(1234, channelId, 1, true);
         },
         ));

         await _engine.enableVideo();

         await _engine.startPreview();
         await _fetchToken(1234, channelId, 1, true);

         setState(() {
         _isReadyPreview = true;
         });
       }

       Future<void> _fetchToken(
         int uid,
         String channelName,
         int toeknRole,
         bool needJoinChannel,
       ) async {
         var client = http.Client();
         try {
         Map<String, String> headers = {
           'Content-type': 'application/json',
           'Accept': 'application/json',
         };

         var response = await client.post(Uri.parse(hostUrl),
           headers: headers,
           body: jsonEncode(
             {'uid': uid, 'ChannelName': channelName, 'role': toeknRole}));
         var decodedResponse = jsonDecode(utf8.decode(response.bodyBytes)) as Map;

         final token = decodedResponse['token'];
         if (needJoinChannel) {
           await _engine.joinChannel(
           token: token,
           channelId: channelName,
           uid: uid,
           options: const ChannelMediaOptions(
             channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
             clientRoleType: ClientRoleType.clientRoleBroadcaster,
           ),
           );
         } else {
           await _engine.renewToken(token);
         }
         } finally {
         client.close();
         }
       }

       @override
       Widget build(BuildContext context) {
         if (!_isReadyPreview) return Container();
         return Stack(
         children: [
           AgoraVideoView(
           controller: VideoViewController(
             rtcEngine: _engine,
             canvas: const VideoCanvas(uid: 0),
           ),
           ),
           Align(
           alignment: Alignment.topLeft,
           child: SingleChildScrollView(
             scrollDirection: Axis.horizontal,
             child: Row(
             children: List.of(remoteUid.map(
               (e) => SizedBox(
               width: 120,
               height: 120,
               child: AgoraVideoView(
                 controller: VideoViewController.remote(
                 rtcEngine: _engine,
                 canvas: VideoCanvas(uid: e),
                 connection: const RtcConnection(channelId: channelId),
                 ),
               ),
               ),
             )),
             ),
           ),
           )
         ],
         );
       }
       }
   ```

   Replace `<Your app ID>` with your app ID, which must be consistent with the app ID you specified in the server configuration. Update `<Your host URL and port>` with the host URL and port of the local Golang server you have deployed. For example `99.9.9.99:8082`.

The sample code implements the following logic:

* Calls `joinChannel` to join a channel using the user ID, the channel name, and a token you obtain from the server. The user ID and channel name you specify must be consistent with the values you used to generate the token.

* The SDK triggers an `onTokenPrivilegeWillExpire` callback 30 seconds before the token expires. After receiving the callback, you obtain a new token from the server and call `renewToken` to pass the newly generated token to the SDK.

* If the token expires, the SDK triggers an `onRequestToken` callback. After receiving the callback, obtain a new token from the server and call `joinChannel` with the new token to rejoin the channel.

Build and run the project on the local device, the app performs the following operations:

* Obtains a token from your token server.
* Joins the channel.
* Automatically renews the token when it is about to expire.

<CalloutContainer type="info">
  <CalloutDescription>
    The user ID and channel name used to join a channel must be consistent with the values used to generate the token.
  </CalloutDescription>
</CalloutContainer>

## Reference

This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

### API reference

* [renewToken](https://api-ref.agora.io/en/video-sdk/flutter/6.x/API/class_irtcengine.html#api_irtcengine_renewtoken)

* [onTokenPrivilegeWillExpire](https://api-ref.agora.io/en/video-sdk/flutter/6.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_ontokenprivilegewillexpire)

* [onConnectionStateChanged](https://api-ref.agora.io/en/video-sdk/flutter/6.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onconnectionstatechanged)

    
  
      
  
      
  
      
  
      
  
      
  
      
  
      
  
      
  
