Security within video chat applications is a hot topic at the moment. As remote working and virtual events become more prolific, the need for security will increase.
Within the Agora Platform, one layer of security comes in the form of token authentication. A token, for those of you that don’t know, is a dynamic key that is generated using a set of given inputs. Agora’s Platform uses tokens to authenticate users.
Agora offers token security for both its RTC and RTM SDKs. This guide will explain how to build a simple microservice using Golang and the Gin framework to generate an Agora RTC and RTM tokens.
Pre Requisites
- A basic understanding of Golang (minimal knowledge needed)
- An understanding of how web servers function (minimal knowledge needed)
- An Agora Developer Account (see: How To Get Started with Agora)
Project Setup
To start, let’s open our terminal and create a new folder for our project and cd into it.
mkdir agora-token-server
cd agora-token-server
Now that the project has been created, let’s initialize the project’s Go module.
go mod init agora-token-server
Lastly, we’ll use go get
to add our Gin and Agora dependencies.
go get github.com/gin-gonic/gin
go get github.com/AgoraIO-Community/go-tokenbuilder
Build the Gin web server
Now that the project is set up, open the folder in your favorite code editor and create the main.go
file.
Within the main.go
we’ll start by declaring our package and adding the main
function.
package main
func main() {
}
Next we’ll import the Gin framework, create our Gin app, set up a simple GET
endpoint and set it to listen and serve on localhost
port 8080
. For the simple endpoint, we’ll set it to take the request context and return a JSON response with a 200
status header.
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
api := gin.Default()
api.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
api.Run(":8080") // listen and serve on localhost:8080
}
We are ready to test our server. Go back to the terminal window and run:
go run main.go
To test the endpoint open your web browser and visit:
localhost:8080/ping
You’ll see the server respond as expected.
{"message":"pong"}
After we confirm that our endpoint is working, return to the terminal window and use the keyboard command ctrl c
to terminate the process.
Generate the Agora Tokens
Now that we have our Gin server setup, we are ready to add the functionality to generate the RTC and RTM tokens.
Before we can generate our tokens we need to add our AppID
and AppCertificate
. We’ll declare the appID
and appCertificate
as Strings in the global
scope. For this guide, use environment variables to store the project credentials, so we’ll need to retrieve them. Within main()
we’ll use os.LookupEnv
to retrieve the environment variables. The os.LookupEnv
returns a String for the environment variable along with a boolean for whether the variable existed. We’ll use the latter return values to check if the environment is configured correctly. If so we can assign the environment variable values to our global appID
and AppCertificate
variables, respectively.
package main
import (
"log"
"os"
"github.com/gin-gonic/gin"
)
var appID, appCertificate string
func main() {
appIDEnv, appIDExists := os.LookupEnv("APP_ID")
appCertEnv, appCertExists := os.LookupEnv("APP_CERTIFICATE")
if !appIDExists || !appCertExists {
log.Fatal("FATAL ERROR: ENV not properly configured, check APP_ID and APP_CERTIFICATE")
} else {
appID = appIDEnv
appCertificate = appCertEnv
}
api := gin.Default()
api.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
api.Run(":8080")
}
Next we will add 3 endpoints, one for RTC tokens, another for RTM tokens, and one that returns both tokens.
The RTC token will require a channel name, the UID, the user role, tokentype to distinguish between string and integer based UIDs, and lastly an expiration time. The RTM endpoint only requires a UID and an expiration time. The dual token endpoint will need to accept the same structure as the RTC token endpoint.
api.GET("rtc/:channelName/:role/:tokentype/:uid/", getRtcToken)
api.GET("rtm/:uid/", getRtmToken)
api.GET("rte/:channelName/:role/:tokentype/:uid/", getBothTokens)
To minimize the amount of repeated code, the three functions getRtcToken
, getRtmToken
, and getBothTokens
will call separate functions (parseRtcParams
/parseRtmParams
) to validate and extract the values passed to each endpoint. Then each function will use the returned values to generate the tokens and return them as JSON in the response body
.
RTC tokens can be generated using two types of UIDs (uint
/string
), so we’ll use a function (generateRtcToken
) to wrap the Agora RTC Token Builder functions BuildTokenWithUserAccount
/BuildTokenWithUID
.
Below is the base template for our token server. We’ll walk through each function and fill in the blanks.
package main
import (
"log"
"os"
"github.com/AgoraIO-Community/go-tokenbuilder/rtctokenbuilder"
"github.com/gin-gonic/gin"
)
var appID, appCertificate string
func main() {
appIDEnv, appIDExists := os.LookupEnv("APP_ID")
appCertEnv, appCertExists := os.LookupEnv("APP_CERTIFICATE")
if !appIDExists || !appCertExists {
log.Fatal("FATAL ERROR: ENV not properly configured, check APP_ID and APP_CERTIFICATE")
} else {
appID = appIDEnv
appCertificate = appCertEnv
}
api := gin.Default()
api.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
api.GET("rtc/:channelName/:role/:tokenType/:uid/", getRtcToken)
api.GET("rtm/:uid/", getRtmToken)
api.GET("rte/:channelName/:role/:tokenType/:uid/", getBothTokens)
api.Run(":8080")
}
func getRtcToken(c *gin.Context) {
}
func getRtmToken(c *gin.Context) {
}
func getBothTokens(c *gin.Context) {
}
func parseRtcParams(c *gin.Context) (channelName, tokentype, uidStr string, role rtctokenbuilder.Role, expireTimestamp uint32, err error) {
}
func parseRtmParams(c *gin.Context) (uidStr string, expireTimestamp uint32, err error) {
}
func generateRtcToken(channelName, uidStr, tokentype string, role rtctokenbuilder.Role, expireTimestamp uint32) (rtcToken string, err error) {
}
Build the RTC Token
We’ll start with getRtcToken
. This function takes a reference to the gin.Context
, using it to call parseRtcParams
which will extract the required values. Then using the returned values to call generateRtcToken
to generate the token String. We’ll also include a few checks for errors to make sure there weren’t any issues along the way. Lastly we’ll build the response.
func getRtcToken(c *gin.Context) {
log.Printf("rtc token\n")
// get param values
channelName, tokentype, uidStr, role, expireTimestamp, err := parseRtcParams(c)
if err != nil {
c.Error(err)
c.AbortWithStatusJSON(400, gin.H{
"message": "Error Generating RTC token: " + err.Error(),
"status": 400,
})
return
}
rtcToken, tokenErr := generateRtcToken(channelName, uidStr, tokentype, role, expireTimestamp)
if tokenErr != nil {
log.Println(tokenErr) // token failed to generate
c.Error(tokenErr)
errMsg := "Error Generating RTC token - " + tokenErr.Error()
c.AbortWithStatusJSON(400, gin.H{
"status": 400,
"error": errMsg,
})
} else {
log.Println("RTC Token generated")
c.JSON(200, gin.H{
"rtcToken": rtcToken,
})
}
}
Next let’s fill in parseRtcParams
. This function also takes a reference to the gin.Context, which we’ll use to extract the parameters and return them. You’ll notice parseRtcParams
also returns an error in case we run into any issues we can return an error message.
func parseRtcParams(c *gin.Context) (channelName, tokentype, uidStr string, role rtctokenbuilder.Role, expireTimestamp uint32, err error) {
// get param values
channelName = c.Param("channelName")
roleStr := c.Param("role")
tokentype = c.Param("tokentype")
uidStr = c.Param("uid")
expireTime := c.DefaultQuery("expiry", "3600")
if roleStr == "publisher" {
role = rtctokenbuilder.RolePublisher
} else {
role = rtctokenbuilder.RoleSubscriber
}
expireTime64, parseErr := strconv.ParseUint(expireTime, 10, 64)
if parseErr != nil {
// if string conversion fails return an error
err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr)
}
// set timestamps
expireTimeInSeconds := uint32(expireTime64)
currentTimestamp := uint32(time.Now().UTC().Unix())
expireTimestamp = currentTimestamp + expireTimeInSeconds
return channelName, tokentype, uidStr, role, expireTimestamp, err
}
Lastly, we’ll fill in the generateRtcToken
function. This function takes the channel name, the UID as a String, the type of token (uid
or userAccount
), the role, and the expire time.
Using these values, the function calls the appropriate Agora RTC Token Builder function (BuildTokenWithUserAccount
/BuildTokenWithUID
) to generate a token String. Once the token builder function returns we’ll first check for errors and if there aren’t any we’ll return the token String value.
func generateRtcToken(channelName, uidStr, tokentype string, role rtctokenbuilder.Role, expireTimestamp uint32) (rtcToken string, err error) {
if tokentype == "userAccount" {
log.Printf("Building Token with userAccount: %s\n", uidStr)
rtcToken, err = rtctokenbuilder.BuildTokenWithUserAccount(appID, appCertificate, channelName, uidStr, role, expireTimestamp)
return rtcToken, err
} else if tokentype == "uid" {
uid64, parseErr := strconv.ParseUint(uidStr, 10, 64)
// check if conversion fails
if parseErr != nil {
err = fmt.Errorf("failed to parse uidStr: %s, to uint causing error: %s", uidStr, parseErr)
return "", err
}
uid := uint32(uid64) // convert uid from uint64 to uint 32
log.Printf("Building Token with uid: %d\n", uid)
rtcToken, err = rtctokenbuilder.BuildTokenWithUID(appID, appCertificate, channelName, uid, role, expireTimestamp)
return rtcToken, err
} else {
err = fmt.Errorf("failed to generate RTC token for Unknown Tokentype: %s", tokentype)
log.Println(err)
return "", err
}
}
Build the RTM token
Next, let’s move on to getRtmToken
. Just like the code above, getRtmToken
takes a reference to the gin.Context
, uses it to call parseRtmParams
to extract the required values, and uses the returned values to generate an RTM token. The difference here is that we call the Agora RTM Token builder directly to generate the token, String. We’ll include the error checks to make sure there weren’t any issues, and lastly we’ll build the response.
func getRtmToken(c *gin.Context) {
log.Printf("rtm token\n")
// get param values
uidStr, expireTimestamp, err := parseRtmParams(c)
if err != nil {
c.Error(err)
c.AbortWithStatusJSON(400, gin.H{
"message": "Error Generating RTC token: " + err.Error(),
"status": 400,
})
return
}
rtmToken, tokenErr := rtmtokenbuilder.BuildToken(appID, appCertificate, uidStr, rtmtokenbuilder.RoleRtmUser, expireTimestamp)
if tokenErr != nil {
log.Println(err) // token failed to generate
c.Error(err)
errMsg := "Error Generating RTM token: " + tokenErr.Error()
c.AbortWithStatusJSON(400, gin.H{
"error": errMsg,
"status": 400,
})
} else {
log.Println("RTM Token generated")
c.JSON(200, gin.H{
"rtmToken": rtmToken,
})
}
}
Next let’s fill in parseRtmParams
. This function also takes a reference to the gin.Context
, then extracts and returns the parameters.
func parseRtmParams(c *gin.Context) (uidStr string, expireTimestamp uint32, err error) {
// get param values
uidStr = c.Param("uid")
expireTime := c.DefaultQuery("expiry", "3600")
expireTime64, parseErr := strconv.ParseUint(expireTime, 10, 64)
if parseErr != nil {
// if string conversion fails return an error
err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr)
}
// set timestamps
expireTimeInSeconds := uint32(expireTime64)
currentTimestamp := uint32(time.Now().UTC().Unix())
expireTimestamp = currentTimestamp + expireTimeInSeconds
// check if string conversion fails
return uidStr, expireTimestamp, err
}
Build both RTC and RTM tokens
Now that we are able to generate both RTC and RTM tokens with individual server requests, we are going to fill in getBothTokens
to allow for generating both tokens from a single request. We’ll use code very similar to the getRtcToken
, except this time we’ll include the RTM token.
func getBothTokens(c *gin.Context) {
log.Printf("dual token\n")
// get rtc param values
channelName, tokentype, uidStr, role, expireTimestamp, rtcParamErr := parseRtcParams(c)
if rtcParamErr != nil {
c.Error(rtcParamErr)
c.AbortWithStatusJSON(400, gin.H{
"message": "Error Generating RTC token: " + rtcParamErr.Error(),
"status": 400,
})
return
}
// generate the rtcToken
rtcToken, rtcTokenErr := generateRtcToken(channelName, uidStr, tokentype, role, expireTimestamp)
// generate rtmToken
rtmToken, rtmTokenErr := rtmtokenbuilder.BuildToken(appID, appCertificate, uidStr, rtmtokenbuilder.RoleRtmUser, expireTimestamp)
if rtcTokenErr != nil {
log.Println(rtcTokenErr) // token failed to generate
c.Error(rtcTokenErr)
errMsg := "Error Generating RTC token - " + rtcTokenErr.Error()
c.AbortWithStatusJSON(400, gin.H{
"status": 400,
"error": errMsg,
})
} else if rtmTokenErr != nil {
log.Println(rtmTokenErr) // token failed to generate
c.Error(rtmTokenErr)
errMsg := "Error Generating RTC token - " + rtmTokenErr.Error()
c.AbortWithStatusJSON(400, gin.H{
"status": 400,
"error": errMsg,
})
} else {
log.Println("RTC Token generated")
c.JSON(200, gin.H{
"rtcToken": rtcToken,
"rtmToken": rtmToken,
})
}
}
Test the Token Server
Let’s go back to our terminal window and run our token server.
run main.go
Once the server instance is running we’ll see the list of endpoints and the message: Listening and serving HTTP on :8080
.
Now that our server instance is running, let’s open our web browser and test. For these tests we’ll try a few variations that omit various query params.
Test the RTC endpoint
We’ll start with the RTC token:
http://localhost:8080/rtc/testing/publisher/userAccount/1234/
http://localhost:8080/rtc/testing/publisher/uid/1234/
The endpoints will generate a token that can be used in the channel: testing
by a user with the role of publisher
and the UID
(String and uint) of 1234
.
{
"rtcToken": "0062ec0d84c41c4442d88ba6f5a2beb828bIADJRwbbO8J93uIDi4J305xNXA0A+pVDTPLPavzwsLW3uAZa8+ij4OObIgDqFTEDoOMyXwQAAQAwoDFfAgAwoDFfAwAwoDFfBAAwoDFf"
}
To test this token we can use the Agora 1:1 Web Demo.
Test the RTM endpoint
Next, we’ll test the RTM token:
http://localhost:8080/rtm/1234/
The endpoints will generate a token that can be used by a user with the UID of 1234
to log into RTM with the given AppID
.
{
"rtmToken": "0062ec0d84c41c4442d88ba6f5a2beb828bIABSMH0fzaqy7sa0erk8u4Bp6FJ4sO1kQ/o6HCRECBRrzKPg45sAAAAAEAAjAkAEO+cyXwEA6APLozFf"
}
To test this token we can use the Agora RTM Tutorial Demo.
Test the Dual token endpoint
We’ll finish our testing with the Dual token endpoint:
http://localhost:8080/rte/testing/publisher/userAccount/1234/
http://localhost:8080/rte/testing/publisher/uid/1234/
The endpoints will generate both RTC
and RTM
tokens that can be used by a user with the UID (String or uint) of 1234
and for the Video channel: testing
with the role of publisher
.
{
"rtcToken": "0062ec0d84c41c4442d88ba6f5a2beb828bIAD33wY6pO+xp6iBY8mbYz2YtOIiRoTTrzdIPF9DEFlSIwZa8+ij4OObIgAQ6e0EX+UyXwQAAQDvoTFfAgDvoTFfAwDvoTFfBADvoTFf",
"rtmToken": "0062ec0d84c41c4442d88ba6f5a2beb828bIABbCwQgl2te3rk0MEDZ2xrPoalb37fFhTqmTIbGeWErWaPg45sAAAAAEAD1WwYBX+UyXwEA6APvoTFf"
}
To test the tokens we can use the Agora 1:1 Web Demo for the RTC token and Agora RTM Tutorial Demo for the RTM token.
After testing the endpoints, your terminal window will display all the requests.
Done!
And just like that we are done! In-case you weren’t coding along or want to see the finished product all together, I have uploaded all the code to GitHub: Agora Token Service.
Thanks for taking the time to read my tutorial and if you have any questions please let me know. If you see any room for improvement feel free to fork the repo and make a pull request!
Other Resources
For more information about the Tokens for Agora.io applications, please take a look at the Set up Authentication guide and [Agora Advanced Guide: How to build a Token]f(https://docs.agora.io/en/Video/token_server_go?platform=Go)(Go).
I also invite you to join the Agoira.io Developer Slack community.
Top comments (0)