Skip to content

Commit

Permalink
Merge pull request #37 from AgoraIO-Community/add-chat-token
Browse files Browse the repository at this point in the history
Add Chat Token
  • Loading branch information
maxxfrazer authored Jul 11, 2023
2 parents 4966aa0 + c94fbe5 commit bbd3abc
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 95 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*.so
*.dylib
main
agora-token-server
agora-token-service

# Test binary, built with `go test -c`
*.test
Expand Down
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,11 @@ The `rtc` token endpoint requires a `tokenType` (uid || userAccount), `channelNa

response:
``` json
{"rtcToken":" "}
{"rtcToken":"007rtc-token-djfkaljdla"}
```

## RTM Token ##
### RTM Token ###

The `rtm` token endpoint requires the user's `rtmuid`.
`expiry(optional)` Pass an integer to represent the privelege lifetime in seconds.
**endpoint structure**
Expand All @@ -94,10 +95,10 @@ The `rtm` token endpoint requires the user's `rtmuid`.

response:
``` json
{"rtmToken":" "}
{"rtmToken":"007rtm-token-djfkaljdla"}
```

### Both Tokens ###
### RTM + RTC Tokens ###
The `rte` token endpoint generates both the `rtc` and `rtm` tokens with a single request. This endpoint requires a `tokenType` (uid || userAccount), `channelName`, the user's `rtcuid` (type varies `String/Int` based on `tokenType`) and `rtmuid` which is a `String`. Omitting `rtmuid` will assume it's the same as `rtcuid`.
`expiry(optional)` Pass an integer to represent the token lifetime in seconds.

Expand All @@ -109,12 +110,35 @@ The `rte` token endpoint generates both the `rtc` and `rtm` tokens with a single
response:
``` json
{
"rtcToken":"rtc-token-djfkaljdla",
"rtmToken":"rtm-token-djfkaljdla"
"rtcToken":"007rtc-token-djfkaljdla",
"rtmToken":"007rtm-token-djfkaljdla"
}
```

### Chat Tokens ###

#### endpoint structure ####

app privileges:
```
chat/app/?expiry=3600
```

user privileges:
```
/chat/account/:chatid/?expiry=3600
```

`expiry` is an optional parameter for both.

response:
``` json
{
"chatToken":"007chat-token-djfkaljdla"
}
```

### Contributions
## Contributions

Contributions are welcome, please test any changes to the Go code with the following command:

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/AgoraIO-Community/agora-token-service
go 1.19

require (
github.com/AgoraIO-Community/go-tokenbuilder v1.1.0
github.com/AgoraIO-Community/go-tokenbuilder v1.2.0
github.com/gin-gonic/gin v1.9.1
github.com/joho/godotenv v1.3.0
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/AgoraIO-Community/go-tokenbuilder v1.1.0 h1:4iuyLV0gPq8D9DVtXa1NsP/8Yvmfy+IVtZsU+LfoTGg=
github.com/AgoraIO-Community/go-tokenbuilder v1.1.0/go.mod h1:xqPdaiFG00M1hNN/CCYh8j+NTmkiJsQtqYdf4YAlncA=
github.com/AgoraIO-Community/go-tokenbuilder v1.2.0 h1:Ktlv7n8PhmSap3tgNxjNHO8hj/qydUIj4UOFF8tRxos=
github.com/AgoraIO-Community/go-tokenbuilder v1.2.0/go.mod h1:xqPdaiFG00M1hNN/CCYh8j+NTmkiJsQtqYdf4YAlncA=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
Expand Down
131 changes: 56 additions & 75 deletions service/endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,22 @@ import (

func TestRtcValidAndInvalid(t *testing.T) {

// Create a new gin engine for testing
reqValid, err := http.NewRequest(http.MethodGet, "/rtc/fsda/publisher/uid/0/?expiry=600", nil)
if err != nil {
t.Fatal(err)
tests := []UrlCodePair{
{"/rtc/fsda/publisher/uid/0/?expiry=600", http.StatusOK},
{"/rtc/fsda/publisher/uid//?expiry=600", http.StatusOK},
{"/rtc/fsda/publisher/uid/test/?expiry=600", http.StatusBadRequest},
{"/rtc/fsda/publisher/uid/0/?expiry=failing", http.StatusBadRequest},
}
reqInvalid, err := http.NewRequest(http.MethodGet, "/rtc/fsda/publisher/uid/test/?expiry=600", nil)
if err != nil {
t.Fatal(err)
for _, httpTest := range tests {
testApi, err := http.NewRequest(http.MethodGet, httpTest.url, nil)
if err != nil {
t.Fatal(err)
}
resp := httptest.NewRecorder()
// Call the endpoint
testService.Server.Handler.ServeHTTP(resp, testApi)
assert.Equal(t, httpTest.code, resp.Code, resp.Body)
}
reqInvalidExp, err := http.NewRequest(http.MethodGet, "/rtc/fsda/publisher/uid/0/?expiry=failing", nil)
if err != nil {
t.Fatal(err)
}
// Create a response recorder to inspect the response
resp := httptest.NewRecorder()

// Call the endpoint
testService.Server.Handler.ServeHTTP(resp, reqValid)

assert.Equal(t, http.StatusOK, resp.Code, resp.Body)

resp = httptest.NewRecorder()

// Call the endpoint
testService.Server.Handler.ServeHTTP(resp, reqInvalid)

assert.Equal(t, http.StatusBadRequest, resp.Code)

resp = httptest.NewRecorder()

// Call the endpoint
testService.Server.Handler.ServeHTTP(resp, reqInvalidExp)

assert.Equal(t, http.StatusBadRequest, resp.Code)
}

func TestRtmValidAndInvalid(t *testing.T) {
Expand Down Expand Up @@ -73,54 +55,53 @@ func TestRtmValidAndInvalid(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.Code)
}

func TestRteValidAndInvalid(t *testing.T) {
type UrlCodePair struct {
url string
code int
}

// Create a new gin engine for testing
reqValid, err := http.NewRequest(http.MethodGet, "/rte/channelName/publisher/uid/0/rtmid/?expiry=600", nil)
if err != nil {
t.Fatal(err)
}
// Create a new gin engine for testing
reqValid2, err := http.NewRequest(http.MethodGet, "/rte/channelName/publisher/uid/2345/?expiry=600", nil)
if err != nil {
t.Fatal(err)
func TestChatValidAndInvalid(t *testing.T) {

tests := []UrlCodePair{
{"/chat/app/", http.StatusOK},
{"/chat/account/username/", http.StatusOK},
{"/chat/account/", http.StatusNotFound},
{"/chat/invalid/", http.StatusNotFound},
{"/chat/account/username/?expiry=600", http.StatusOK},
{"/chat/account/username/?expiry=fail", http.StatusBadRequest},
}
reqInvalid, err := http.NewRequest(http.MethodGet, "/rte/channelName/publisher/uid/0/?expiry=600", nil)
if err != nil {
t.Fatal(err)
for _, httpTest := range tests {
testApi, err := http.NewRequest(http.MethodGet, httpTest.url, nil)
if err != nil {
t.Fatal(err)
}
resp := httptest.NewRecorder()
// Call the endpoint
testService.Server.Handler.ServeHTTP(resp, testApi)
assert.Equal(t, httpTest.code, resp.Code, resp.Body)
}
reqInvalidExp, err := http.NewRequest(http.MethodGet, "/rte/channelName/publisher/uid/2345/?expiry=failing", nil)
if err != nil {
t.Fatal(err)
}
// Create a response recorder to inspect the response
resp := httptest.NewRecorder()

// Call the endpoint
testService.Server.Handler.ServeHTTP(resp, reqValid)

assert.Equal(t, http.StatusOK, resp.Code)

resp = httptest.NewRecorder()

// Call the endpoint
testService.Server.Handler.ServeHTTP(resp, reqValid2)

assert.Equal(t, http.StatusOK, resp.Code)

resp = httptest.NewRecorder()

// Call the endpoint
testService.Server.Handler.ServeHTTP(resp, reqInvalid)

assert.Equal(t, http.StatusBadRequest, resp.Code)

resp = httptest.NewRecorder()
}

// Call the endpoint
testService.Server.Handler.ServeHTTP(resp, reqInvalidExp)
func TestRteValidAndInvalid(t *testing.T) {

assert.Equal(t, http.StatusBadRequest, resp.Code)
tests := []UrlCodePair{
{"/rte/channelName/publisher/uid/0/rtmid/?expiry=600", http.StatusOK},
{"/rte/channelName/publisher/uid/2345/?expiry=600", http.StatusOK},
{"/rte/channelName/subscriber/uid/0/rtmid/?expiry=600", http.StatusOK},
{"/rte/channelName/subscriber/uid/2345/?expiry=600", http.StatusOK},
{"/rte/channelName/publisher/uid/0/?expiry=600", http.StatusBadRequest},
{"/rte/channelName/publisher/uid/2345/?expiry=failing", http.StatusBadRequest},
}
for _, httpTest := range tests {
testApi, err := http.NewRequest(http.MethodGet, httpTest.url, nil)
if err != nil {
t.Fatal(err)
}
resp := httptest.NewRecorder()
// Call the endpoint
testService.Server.Handler.ServeHTTP(resp, testApi)
assert.Equal(t, httpTest.code, resp.Code, resp.Body)
}
}

func TestTokentypes(t *testing.T) {
Expand Down
32 changes: 31 additions & 1 deletion service/http_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (s *Service) getRtmToken(c *gin.Context) {
if err != nil {
c.Error(err)
c.AbortWithStatusJSON(400, gin.H{
"message": "Error Generating RTC token: " + err.Error(),
"message": "Error Generating RTM token: " + err.Error(),
"status": 400,
})
return
Expand All @@ -72,6 +72,36 @@ func (s *Service) getRtmToken(c *gin.Context) {
}
}

func (s *Service) getChatToken(c *gin.Context) {
log.Println("Generating Chat token")
// get param values
uidStr, tokenType, expireTimestamp, err := s.parseChatParams(c)

if err != nil {
c.AbortWithStatusJSON(400, gin.H{
"message": "Error Generating Chat token: " + err.Error(),
"status": 400,
})
return
}

chatToken, tokenErr := s.generateChatToken(uidStr, tokenType, expireTimestamp)

if tokenErr != nil {
c.Error(tokenErr)
errMsg := "Error Generating Chat token: " + tokenErr.Error()
c.AbortWithStatusJSON(400, gin.H{
"error": errMsg,
"status": 400,
})
} else {
log.Println("Chat Token generated")
c.JSON(200, gin.H{
"chatToken": chatToken,
})
}
}

func (s *Service) getRtcRtmToken(c *gin.Context) {
log.Println("Generating RTC and RTM tokens")
// get rtc param values
Expand Down
7 changes: 4 additions & 3 deletions service/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ var testService *Service
func TestMain(m *testing.M) {

gin.SetMode(gin.TestMode)
// os.Setenv("APP_ID", "example-app-id")
// os.Setenv("APP_CERTIFICATE", "example-app-certificate")
os.Setenv("SERVER_PORT", "8080")
// os.Setenv("APP_ID", "18c2dfa12345678987654321fb84931d")
// os.Setenv("APP_CERTIFICATE", "12345712345678765432117b84ad9ef9")
// os.Setenv("SERVER_PORT", "8080")
// os.Setenv("PORT", "8080")
testService = NewService()
os.Exit(m.Run())
}
39 changes: 34 additions & 5 deletions service/parsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package service
import (
"fmt"
"strconv"
"strings"
"time"

rtctokenbuilder2 "github.com/AgoraIO-Community/go-tokenbuilder/rtctokenbuilder"
Expand Down Expand Up @@ -37,11 +38,7 @@ func (s *Service) parseRtcParams(c *gin.Context) (channelName, tokenType, uidStr
expireTime64, parseErr := strconv.ParseUint(expireTime, 10, 64)
if parseErr != nil {
// if string conversion fails return an error
if err != nil {
err = fmt.Errorf("%s. Also failed to parse expireTime: %s, causing error: %s", err, expireTime, parseErr)
} else {
err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr)
}
err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr)
}

// set timestamps
Expand Down Expand Up @@ -75,3 +72,35 @@ func (s *Service) parseRtmParams(c *gin.Context) (uidStr string, expireTimestamp
// check if string conversion fails
return uidStr, expireTimestamp, err
}

func (s *Service) parseChatParams(c *gin.Context) (uidStr string, tokenType string, expireTimestamp uint32, err error) {
// get param values
uidStr = c.Param("chatid")
urlSplit := strings.Split(c.Request.URL.Path, "/")
for i := range urlSplit {
if urlSplit[i] == "chat" {
tokenType = urlSplit[i+1]
break
}
}
expireTime := c.DefaultQuery("expiry", "3600")
if tokenType == "account" {
tokenType = "userAccount"
}
if uidStr == "" && tokenType != "app" {
err = fmt.Errorf("userAccount type requires chat ID")
return uidStr, tokenType, expireTimestamp, err
}
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, tokenType, expireTimestamp, err
}
2 changes: 2 additions & 0 deletions service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ func NewService() *Service {
api.GET("rtm/:rtmuid/", s.getRtmToken)
api.GET("rte/:channelName/:role/:tokenType/:rtcuid/", s.getRtcRtmToken)
api.GET("rte/:channelName/:role/:tokenType/:rtcuid/:rtmuid/", s.getRtcRtmToken)
api.GET("chat/app/", s.getChatToken) // Chat token for API calls
api.GET("chat/account/:chatid/", s.getChatToken) // Chat token for SDK calls

s.Server.Handler = api
return s
Expand Down
18 changes: 18 additions & 0 deletions service/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log"
"strconv"

"github.com/AgoraIO-Community/go-tokenbuilder/chatTokenBuilder"
rtctokenbuilder2 "github.com/AgoraIO-Community/go-tokenbuilder/rtctokenbuilder"
)

Expand Down Expand Up @@ -33,3 +34,20 @@ func (s *Service) generateRtcToken(channelName, uidStr, tokenType string, role r
return "", err
}
}

func (s *Service) generateChatToken(uidStr string, tokenType string, expireTimestamp uint32) (chatToken string, err error) {

if tokenType == "userAccount" {
log.Printf("Building Token with userAccount: %s\n", uidStr)
chatToken, err = chatTokenBuilder.BuildChatUserToken(s.appID, s.appCertificate, uidStr, expireTimestamp)
return chatToken, err

} else if tokenType == "app" {
chatToken, err = chatTokenBuilder.BuildChatAppToken(s.appID, s.appCertificate, expireTimestamp)
return chatToken, err
} else {
err = fmt.Errorf("failed to generate Chat token for Unknown token type: %s", tokenType)
log.Println(err)
return "", err
}
}

0 comments on commit bbd3abc

Please sign in to comment.