Home > Back-end >  Why can't my iOS app authenticate with AgoraRtcEngineKit using tokens created by a Node.js Agor
Why can't my iOS app authenticate with AgoraRtcEngineKit using tokens created by a Node.js Agor

Time:12-02

PROBLEM SUMMARY

My goal is to use an iOS app written in SwiftUI to connect with AgoraRtcEngineKit. I want to create an app that is audio-only and allows a host to broadcast audio and allows listeners to listen in.

The use of tokens is required by Agora.

I created an Agora Token Server using Node.js based on Agora's tutorial found here: https://www.agora.io/en/blog/how-to-build-a-token-server-for-agora-applications-using-nodejs/

Here is my index.js from my Agora-Node-TokenServer. This code is based on the Agora tutorial found here: https://github.com/digitallysavvy/Agora-Node-TokenServer/blob/master/index.js

const express = require('express')
const path = require('path')
const {RtcTokenBuilder, RtcRole} = require('agora-access-token');

const PORT = process.env.PORT || 5000

if (!(process.env.APP_ID && process.env.APP_CERTIFICATE)) {
  throw new Error('You must define an APP_ID and APP_CERTIFICATE');
}
const APP_ID = process.env.APP_ID;
const APP_CERTIFICATE = process.env.APP_CERTIFICATE;

const app = express();

const nocache = (req, resp, next) => {
  resp.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
  resp.header('Expires', '-1');
  resp.header('Pragma', 'no-cache');
  next();
  };


const generateAccessToken = (req, resp) => { 
  resp.header('Access-Control-Allow-Origin', '*');

  const channelName = req.query.channelName;if (!channelName) {
    return resp.status(500).json({ 'error': 'channel is required' });
  }

  
  // get uid 
  let uid = req.query.uid;
  if(!uid || uid == '') {
    uid = 0;
  }

  // get rtc role
  let rtcrole = RtcRole.SUBSCRIBER;
  if (req.query.rtcrole == 'publisher') {
    rtcrole = RtcRole.PUBLISHER;
  }


  // get the expire time
  let expireTime = req.query.expireTime;
  if (!expireTime || expireTime == '') {
    expireTime = 3600;
  } else {
    expireTime = parseInt(expireTime, 10);
  }
  // calculate privilege expire time
  const currentTime = Math.floor(Date.now() / 1000);
  const privilegeExpireTime = currentTime   expireTime;

  const rtctoken = RtcTokenBuilder.buildTokenWithUid(APP_ID, APP_CERTIFICATE, channelName, uid, rtcrole, privilegeExpireTime);
 
  return resp.json({ 'rtctoken': rtctoken });

};

app.get('/access_token', nocache, generateAccessToken);

app.listen(PORT, () => {
  console.log(`Listening on port: ${PORT}`);
});

What I expected What I expected was to be able to successfully authenticate my iOS app with Agora.

Actual results I am unable to authenticate with Agora.

  1. My Node.js token server successfully delivers a token to my iOS app.
  2. But when I submit said token to Agora using rtckit.joinChannel(byToken:...) nothing happens. The joinChannel's completion block is never reached.
  3. As an experiment I submitted the 'temporary token' from Agora's console to Agora using rtckit.joinChannel(byToken:...) and this token was accepted successfully and the completion block was reached.

What I have tried

  • My iOS app is able to connect with Agora by using Temporary Tokens created using Agora's console. Agora's console allows developers to create Temporary Tokens to test their apps. Since my app is able to authenticate using these temporary tokens it suggest to me the problem lies somewhere with the NodeJS token server I created?

  • I have reviewed similar questions on Stack Overflow like:

    1. how to generate token for agora RTC for live stream and join channel
    2. Made sure my APP_ID and APP_CERTIFICATE matched those in Agora's console as recommended here: Agora Video Calling Android get error code 101

For reference here is the relevant code from my iOS app below. I based this code on the Agora Tutorial found here: https://github.com/maxxfrazer/Agora-iOS-Swift-Example/blob/main/Agora-iOS-Example/AgoraToken.swift and here: https://www.agora.io/en/blog/creating-live-audio-chat-rooms-with-swiftui/

AgoraToken

import Foundation

class AgoraToken {

    /// Error types to expect from fetchToken on failing ot retrieve valid token.
    enum TokenError: Error {
        case noData
        case invalidData
        case invalidURL
    }

    /// Requests the token from our backend token service
    /// - Parameter urlBase: base URL specifying where the token server is located
    /// - Parameter channelName: Name of the channel we're requesting for
    /// - Parameter uid: User ID of the user trying to join (0 for any user)
    /// - Parameter callback: Callback method for returning either the string token or error
    static func fetchToken(
        urlBase: String, channelName: String, uid: UInt,
        callback: @escaping (Result<String, Error>) -> Void
    ) {
        guard let fullURL = URL(string: "\(urlBase)/?channelName=\(channelName)/&uid=\(uid)/") else {
            callback(.failure(TokenError.invalidURL))
            return
        }
        print("fullURL yields \(fullURL)")
        var request = URLRequest(
            url: fullURL,
            timeoutInterval: 10
        )
        request.httpMethod = "GET"

        let task = URLSession.shared.dataTask(with: request) { data, response, err in
            print("Within URLSession.shared.dataTask response is \(String(describing: response)) and err is \(String(describing: err))")
            if let dataExists = data {
                print(String(bytes: dataExists, encoding: String.Encoding.utf8) ?? "URLSession no data exists")
            }

            guard let data = data else {
                if let err = err {
                    callback(.failure(err))
                } else {
                    callback(.failure(TokenError.noData))
                }
                return
            }
            let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
            if let responseDict = responseJSON as? [String: Any], let rtctoken = responseDict["rtctoken"] as? String {
                print("rtc token is \(rtctoken)")
                callback(.success(rtctoken))
            } else {
                callback(.failure(TokenError.invalidData))
            }
        }

        task.resume()
    }
    
}

Podfile

# Uncomment the next line to define a global platform for your project
platform :ios, '14.8.1'

target 'AgoraPractice' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for AgoraPractice
  pod 'AgoraRtm_iOS' 
  pod 'AgoraAudio_iOS' 

  target 'AgoraPracticeTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'AgoraPracticeUITests' do
    # Pods for testing
  end

end

ContentView

import SwiftUI
import CoreData
import AgoraRtcKit



struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @State var joinedChannel: Bool = false
    @ObservedObject var agoraObservable = AgoraObservable()
    
       var body: some View {
           Form {
               Section(header: Text("Channel Information")) {
                   TextField(
                       "Channel Name", text: $agoraObservable.channelName
                   ).disabled(joinedChannel)
                   TextField(
                       "Username", text: $agoraObservable.username
                   ).disabled(joinedChannel)
               }
               Button(action: {
                   joinedChannel.toggle()
                   if !joinedChannel {
                       self.agoraObservable.leaveChannel()
                   } else {
                       self.agoraObservable.checkIfChannelTokenExists()
                   }
               }, label: {
                   Text("\(joinedChannel ? "Leave" : "Join") Channel")
                       .accentColor(joinedChannel ? .red : .blue)
               })
               if joinedChannel {

                   Button(action: {
                       agoraObservable.rtckit.setClientRole(.audience)
                   }, label: {
                       Text("Become audience member")
                   })

                   Button(action: {
                       agoraObservable.rtckit.setClientRole(.broadcaster)
                   }, label: {
                       Text("Become broadcasting member")
                   })

               }
           }
       }
   }

   struct UserData: Codable {
       var rtcId: UInt
       var username: String
       func toJSONString() throws -> String? {
           let jsonData = try JSONEncoder().encode(self)
           return String(data: jsonData, encoding: .utf8)
       }
   }

   class AgoraObservable: NSObject, ObservableObject {
       
       @Published var remoteUserIDs: Set<UInt> = []
       @Published var channelName: String = ""
       @Published var username: String = ""

       
       // Temp Token and Channel Name
       var tempToken:String = "XXXXXXX"
       var tempChannelName:String = "XXXXX"
       
       var rtcId: UInt = 0
       //var rtcId: UInt = 1264211369
       
       let tokenBaseURL:String = Secrets.baseUrl
       
       // channelToken is rtctoken.  I should change this variable name at some point to rtctoken...
       var channelToken:String = ""
       
       lazy var rtckit: AgoraRtcEngineKit = {
           let engine = AgoraRtcEngineKit.sharedEngine(
            withAppId: Secrets.agoraAppId, delegate: self
           )
           engine.setChannelProfile(.liveBroadcasting)
           engine.setClientRole(.audience)
           return engine
       }()
    
       
   }

   extension AgoraObservable {
       
       func checkIfChannelTokenExists() {
           if channelToken.isEmpty {
               joinChannelWithFetch()
           } else {
               joinChannel()
           }
       }
       
       func joinChannelWithFetch() {
           
           AgoraToken.fetchToken(
                       urlBase: tokenBaseURL,
                       channelName: self.channelName,
                       uid: self.rtcId
                   ) { result in
                       switch result {
                       case .success(let tokenExists):
                           self.channelToken = tokenExists
                           print("func joinChannelWithFetch(): channelToken = \(self.channelToken) and rtcuid = \(self.rtcId)")
                           self.joinChannel()
                       case .failure(let err):
                           print(err)
                           // To Do: Handle this error with an alert
                           self.leaveChannel()
                           }
                       }
       }
       
       func joinChannel(){
           print("func joinChannel(): channelToken = \(self.channelToken) and channelName = \(self.channelName)  and rtcuid = \(self.rtcId)")

           self.rtckit.joinChannel(byToken: self.channelToken, channelId: self.channelName, info: nil, uid: self.rtcId) { [weak self] (channel, uid, errCode) in
               print("within rtckit.joinchannel yields: channel:\(channel) and uid:\(uid) and error:\(errCode)")
               
               self?.rtcId = uid
               
           }
           // I need to error handle if user cannot loginto rtckit
       }
       
       func updateToken(_ newToken:String){
           channelToken = newToken
           self.rtckit.renewToken(newToken)
           print("Updating token now...")
       }
       
       func leaveChannel() {
           self.rtckit.leaveChannel()
       }
       
  }

extension AgoraObservable: AgoraRtcEngineDelegate {
    /// Called when the user role successfully changes
       /// - Parameters:
       ///   - engine: AgoraRtcEngine of this session.
       ///   - oldRole: Previous role of the user.
       ///   - newRole: New role of the user.
       func rtcEngine(
           _ engine: AgoraRtcEngineKit,
           didClientRoleChanged oldRole: AgoraClientRole,
           newRole: AgoraClientRole
       ) {
           
           print("AgoraRtcEngineDelegate didClientRoleChanged triggered...old role: \(oldRole), new role: \(newRole)")

       }

       func rtcEngine(
           _ engine: AgoraRtcEngineKit,
           didJoinedOfUid uid: UInt,
           elapsed: Int
       ) {
           // Keeping track of all people in the session
           print("rtcEngine didJoinedOfUid triggered...")
           remoteUserIDs.insert(uid)
       }

       func rtcEngine(
           _ engine: AgoraRtcEngineKit,
           didOfflineOfUid uid: UInt,
           reason: AgoraUserOfflineReason
       ) {
           print("rtcEngine didOfflineOfUid triggered...")
           // Removing on quit and dropped only
           // the other option is `.becomeAudience`,
           // which means it's still relevant.
           if reason == .quit || reason == .dropped {
               remoteUserIDs.remove(uid)
           } else {
               // User is no longer hosting, need to change the lookups
               // and remove this view from the list
               // userVideoLookup.removeValue(forKey: uid)
           }
       }

       func rtcEngine(
           _ engine: AgoraRtcEngineKit,
           tokenPrivilegeWillExpire token: String
       ) {
           print("tokenPrivilegeWillExpire delegate method called...")
           AgoraToken.fetchToken(
            urlBase: tokenBaseURL, channelName: self.channelName, uid: self.rtcId) { result in
               switch result {
               case .failure(let err):
                   fatalError("Could not refresh token: \(err)")
               case .success(let newToken):
                   print("token successfully updated")
                   self.updateToken(newToken)
               }
           }
       }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

CodePudding user response:

It turns out I made a mistake when I was trying to define the 'fullURL' for my http request to fetch a token. I am new to http requests so I did not know I had made an error.

In my AgoraToken.swift file, my erroneous fullURL definition was:

        guard let fullURL = URL(string: "\(urlBase)/?channelName=\(channelName)/&uid=\(uid)/") else {...

As you will be able to see if you aren't a noob like me with this code channelName and uid will both be sent to my Token Server with trailing slashes. So if my channelName was 'TupperwareParty' my Token Server would get 'TupperwareParty/' and if my uid was '123456' my Token Server would get '123456/'.

I fixed it with this new fullURL definition...

        guard let fullURL = URL(string: "\(urlBase)/?channelName=\(channelName)&uid=\(uid)") else {...

Sigh...

  • Related