Use tokens
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 SD-RTN™ for authentication. Agora SD-RTN™ 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 Video Calling:
Token authentication flow
Prerequisites
Before starting, ensure that you have:
-
Implemented the Quickstart in your project.
-
Deployed a token server using either of the following guides:
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
The client requests a token from your authentication server corresponding to the user ID and the channel name. You use the received token to join a channel.
- Java
- Kotlin
// Channel nameString channelId = "xxx";// User IDint uid = 0;// Request a token from the server corresponding to channelId and uidString token = getToken(channelId, uid);// Set channel media optionsChannelMediaOptions option = new ChannelMediaOptions();option.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER;// Join a channelengine.joinChannel(token, channelId, 0, option);
// Channel nameval channelId = "xxx"// User IDval uid = 0// Request a token from the server corresponding to channelId and uidval token = getToken(channelId, uid)// Set channel media optionsval option = ChannelMediaOptions().apply { clientRoleType = Constants.CLIENT_ROLE_BROADCASTER}// Join a channelengine.joinChannel(token, channelId, 0, option)
Token expiration
After you join a channel using a token, the SDK triggers an onTokenPrivilegeWillExpire
callback, 30 seconds before the token is set to expire.
When the token expires, the SDK triggers an onRequestToken
callback. After receiving the callback, you regenerate a new token on the server side, and then update the token in one of the following ways:
Single channel use-case
-
Call
renewToken
to pass in the newly generated Token (Recommended). -
Call
updateChannelMediaOptions
to update the token. -
Call
leaveChannel
[2/2] to leave the current channel, and then pass in a new token when callingjoinChannel
[2/2] to rejoin the channel.
Multi-channel use-case
If you call joinChannelEx
to join multiple channels, call the updateChannelMediaOptionsEx
method to update the token.
The following sample code demonstrates how to call renewToken
to update the token upon receiving an onTokenPrivilegeWillExpire
callback notification.
- Java
- Kotlin
class RtcEngineEventHandlerImpl extends IRtcEngineEventHandler { // Callback is triggered when the token is about to expire @Override public void onTokenPrivilegeWillExpire(String token) { super.onTokenPrivilegeWillExpire(token); // Request to generate a fresh token String token = getToken(); // Update token engine.renewToken(token); }}
class RtcEngineEventHandlerImpl : IRtcEngineEventHandler() { // Callback triggered when the token is about to expire override fun onTokenPrivilegeWillExpire(token: String) { super.onTokenPrivilegeWillExpire(token) // Request to generate a fresh token val newToken = getToken() // Update the token engine.renewToken(newToken) }}
Complete sample code
This section presents sample code to implement token authentication in your client.
-
In
/Gradle Scripts/build.gradle(Module: <projectname>.app)
, add the following dependencies for making HTTP requests to the token server: -
In
MainActivity
, replace the content with the following code:Complete sample code for token authentication
- Java
- Kotlin
package com.example.rtcquickstart;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import androidx.core.app.ActivityCompat;import androidx.core.content.ContextCompat;import android.Manifest;import android.content.pm.PackageManager;import android.view.SurfaceView;import android.widget.FrameLayout;import android.widget.Toast;import io.agora.rtc2.Constants;import io.agora.rtc2.IRtcEngineEventHandler;import io.agora.rtc2.RtcEngine;import io.agora.rtc2.video.VideoCanvas;import io.agora.rtc2.ChannelMediaOptions;import okhttp3.MediaType;import okhttp3.OkHttpClient;import okhttp3.Request;import okhttp3.RequestBody;import okhttp3.Response;import okhttp3.Call;import okhttp3.Callback;import com.google.gson.Gson;import java.util.Map;import org.json.JSONException;import org.json.JSONObject;import java.io.IOException;public class MainActivity extends AppCompatActivity { // Fill in the App ID from Agora console private String appId = "Your App ID"; // Fill in the channel name private String channelName = "demo"; private String token = ""; private RtcEngine mRtcEngine; private int joined = 1; private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() { @Override public void onUserJoined(int uid, int elapsed) { runOnUiThread(() -> setupRemoteVideo(uid)); } @Override public void onTokenPrivilegeWillExpire(String token) { fetchToken(1234, channelName, 1); runOnUiThread(() -> { Toast toast = Toast.makeText(MainActivity.this, "Token renewed", Toast.LENGTH_SHORT); toast.show(); }); super.onTokenPrivilegeWillExpire(token); } @Override public void onRequestToken() { joined = 1; fetchToken(1234, channelName, 1); super.onRequestToken(); } }; private static final int PERMISSION_REQ_ID = 22; private static final String[] REQUESTED_PERMISSIONS = { Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA }; private boolean checkSelfPermission(String permission, int requestCode) { if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode); return false; } return true; } // Get RTC Token private void fetchToken(int uid, String channelName, int tokenRole) { OkHttpClient client = new OkHttpClient(); MediaType JSON = MediaType.parse("application/json; charset=utf-8"); JSONObject json = new JSONObject(); try { json.put("uid", uid); json.put("ChannelName", channelName); json.put("role", tokenRole); } catch (JSONException e) { e.printStackTrace(); } RequestBody requestBody = RequestBody.create(JSON, String.valueOf(json)); Request request = new Request.Builder() .url("http://<Your Host URL and port>/fetch_rtc_token") .header("Content-Type", "application/json; charset=UTF-8") .post(requestBody) .build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { if (response.isSuccessful()) { Gson gson = new Gson(); String result = response.body().string(); Map map = gson.fromJson(result, Map.class); new Thread(() -> { token = map.get("token").toString(); ChannelMediaOptions options = new ChannelMediaOptions(); if (joined != 0) { joined = mRtcEngine.joinChannel(token, channelName, 1234, options); } else { mRtcEngine.renewToken(token); } }).start(); } } }); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID) && checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID)) { initializeAndJoinChannel(); } } @Override protected void onDestroy() { super.onDestroy(); mRtcEngine.leaveChannel(); mRtcEngine.destroy(); } private void initializeAndJoinChannel() { try { mRtcEngine = RtcEngine.create(getBaseContext(), appId, mRtcEventHandler); } catch (Exception e) { throw new RuntimeException("Check the error."); } mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); mRtcEngine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); mRtcEngine.enableVideo(); FrameLayout container = findViewById(R.id.local_video_view_container); SurfaceView surfaceView = new SurfaceView(getBaseContext()); container.addView(surfaceView); mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, 0)); mRtcEngine.startPreview(); fetchToken(1234, channelName, 1); } private void setupRemoteVideo(int uid) { FrameLayout container = findViewById(R.id.remote_video_view_container); SurfaceView surfaceView = new SurfaceView(getBaseContext()); surfaceView.setZOrderMediaOverlay(true); container.addView(surfaceView); mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, uid)); }}
package com.example.rtcquickstartimport android.Manifest;import android.content.pm.PackageManager;import android.os.Bundle;import android.view.SurfaceView;import android.widget.FrameLayout;import android.widget.Toast;import androidx.appcompat.app.AppCompatActivity;import androidx.core.app.ActivityCompat;import androidx.core.content.ContextCompat;import io.agora.rtc2.*;import io.agora.rtc2.video.VideoCanvas;import com.google.gson.Gson;import okhttp3.*;import org.json.JSONException;import org.json.JSONObject;import java.io.IOException;import java.util.*;class MainActivity : AppCompatActivity() { // Fill in the App ID from Agora console private val appId = "Your App ID" // Fill in the channel name private val channelName = "demo" private var token = "" private var mRtcEngine: RtcEngine? = null private var joined = 1 private val mRtcEventHandler = object : IRtcEngineEventHandler() { override fun onUserJoined(uid: Int, elapsed: Int) { runOnUiThread { // After obtaining the user ID in the onUserJoined callback, call setupRemoteVideo to set the remote user view setupRemoteVideo(uid) } } override fun onTokenPrivilegeWillExpire(token: String) { fetchToken(1234, channelName, 1) runOnUiThread { val toast = Toast.makeText(this@MainActivity, "Token renewed", Toast.LENGTH_SHORT) toast.show() } super.onTokenPrivilegeWillExpire(token) } override fun onRequestToken() { joined = 1 fetchToken(1234, channelName, 1) super.onRequestToken() } } companion object { private const val PERMISSION_REQ_ID = 22 private val REQUESTED_PERMISSIONS = arrayOf( Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA ) } private fun checkSelfPermission(permission: String, requestCode: Int): Boolean { return if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode) false } else { true } } // Get RTC Token private fun fetchToken(uid: Int, channelName: String, tokenRole: Int) { val client = OkHttpClient() val JSON = MediaType.parse("application/json; charset=utf-8") val json = JSONObject() try { json.put("uid", uid) json.put("ChannelName", channelName) json.put("role", tokenRole) } catch (e: JSONException) { e.printStackTrace() } val requestBody = RequestBody.create(JSON, json.toString()) val request = Request.Builder() .url("http://<Your Host URL and port>/fetch_rtc_token") .header("Content-Type", "application/json; charset=UTF-8") .post(requestBody) .build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { // Handle failure } override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { val gson = Gson() val result = response.body?.string() val map: Map<String, Any> = gson.fromJson(result, Map::class.java) Thread { token = map["token"].toString() // If the user has not joined the channel, use token to join the channel val options = ChannelMediaOptions() if (joined != 0) { joined = mRtcEngine?.joinChannel(token, channelName, 1234, options) ?: -1 } else { mRtcEngine?.renewToken(token) } }.start() } } }) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // If all permissions are granted, initialize the RtcEngine object and join the channel if (checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID) && checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID)) { initializeAndJoinChannel() } } override fun onDestroy() { super.onDestroy() mRtcEngine?.leaveChannel() mRtcEngine?.destroy() } private fun initializeAndJoinChannel() { try { mRtcEngine = RtcEngine.create(baseContext, appId, mRtcEventHandler) } catch (e: Exception) { throw RuntimeException("Check the error.") } // In interactive live broadcast, set the channel scene to BROADCASTING mRtcEngine?.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING) // Depending on the actual situation, set the user role to BROADCASTER or AUDIENCE mRtcEngine?.setClientRole(Constants.CLIENT_ROLE_BROADCASTER) // SDK turns off video by default. Call enableVideo to enable video mRtcEngine?.enableVideo() val container: FrameLayout = findViewById(R.id.local_video_view_container) // Call CreateRendererView to create a SurfaceView object and add it as a child to the FrameLayout val surfaceView = SurfaceView(baseContext) container.addView(surfaceView) // Pass the SurfaceView object to Agora to render local video mRtcEngine?.setupLocalVideo(VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, 0)) // Start local video preview mRtcEngine?.startPreview() // Get a token from the token server fetchToken(1234, channelName, 1) } private fun setupRemoteVideo(uid: Int) { val container: FrameLayout = findViewById(R.id.remote_video_view_container) val surfaceView = SurfaceView(baseContext) surfaceView.setZOrderMediaOverlay(true) container.addView(surfaceView) mRtcEngine?.setupRemoteVideo(VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, uid)) }}
The user ID and channel name used to join a channel must be consistent with the values used to generate the token.
Reference
This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.