1
votes

I'm trying to send multiple web notifications using topic instead of looping send notification to each token.

But I stuck at sending notification to topic using Credential with 401 unauthorized

Here is my code

Firstly, I request web browser permission for notification token via this Firebase sample code: https://github.com/firebase/quickstart-js/blob/master/messaging

On index.html I change code like this to send token to server for subscribing to topic

    function resetUI() {
        clearMessages();

        // [START get_token]
        // Get Instance ID token. Initially this makes a network call, once retrieved
        // subsequent calls to getToken will return from cache.
        messaging.getToken().then(function (currentToken) {
            if (currentToken) {
                console.log("Get Token OK: " + currentToken);
                sendTokenToServer(currentToken);
            } else {
                // Show permission request.
                console.log('No Instance ID token available. Request permission to generate one.');
                // Reset Token on Server to False
                setTokenSentToServer(false);

                // Request permission for Token
                requestPermission();
            }
        }).catch(function (err) {
            console.log('An error occurred while retrieving token. ', err);

            setTokenSentToServer(false);
        });
        // [END get_token]
    }


    // Send the Instance ID token your application server, so that it can:
    // - send messages back to this app
    // - subscribe/unsubscribe the token from topics
    function sendTokenToServer(currentToken) {
        if (!(window.localStorage.getItem('EDXNotificationIsSentToServer') === '1')) {
            console.log('Sending token to server...');

            var tokenObject = {
                token: currentToken
            };

            $.ajax({
                type: "POST",
                url: '@Url.Action("SaveToken", "Api")',
                data: JSON.stringify(tokenObject),
                contentType: "application/json; charset=utf-8",
                success: function (data) {
                    if (data) {
                        // TODO(developer): Send the current token to your server.
                        setTokenSentToServer(true);
                    }
                },
                complete: function (status) {
                },
                error: function (error) { }
            });

        } else {
            console.log('Token already sent to server so won\'t send it again unless it changes');
        }

    }

On server, I stored token to database and subscribe it to test topic

    public class ApiController : Controller
    {
        private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        /// <summary>
        /// Lưu token
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        public JsonResult SaveToken(TBL_Customer_Tokens model)
        {
            try
            {
                if (!string.IsNullOrEmpty(model.token))
                {
                    bool ret = true;

                    if(GCMUtils.SubcribeTokenToTopic(model.token, ConfigurationManager.AppSettings["GCMTopic"]))
                        ret = DataServiceFactory.GetCustomerService().SaveToken(model.token); // Save token to database, don't care about this function. It is ok

                    if (ret)
                        Json(true, JsonRequestBehavior.AllowGet);
                }
            }
            catch (Exception ex)
            {
                log.Debug(ex.Message, ex);
            }

            return Json(false, JsonRequestBehavior.AllowGet);
        }
    }

Here is GCMUtils

    public class GCMUtils
    {
        private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);


        public static bool SubcribeTokenToTopic(string token, string topic)
        {
            try
            {
                string url = String.Format("https://iid.googleapis.com/iid/v1/{0}/rel/topics/{1}", token, topic);

                HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);

                req.ContentType = "application/json";
                req.Headers.Add("Authorization", "key=" + ConfigurationManager.AppSettings["GCMServerKey"]);


                req.Method = "POST";
                req.ContentLength = 0;

                Stream reqStream = req.GetRequestStream();
                reqStream.Close();

                HttpWebResponse response = (HttpWebResponse)req.GetResponse();

                if (response.StatusCode == HttpStatusCode.OK)
                    return true;
            }
            catch (Exception ex)
            {
                log.Debug(ex.Message, ex);
            }

            return false;
        }
    }

Token is retrieved, saved to database and successfully subscribed to topic. So you may ignore above code, analyze only problem belows

Secondly, I'm trying to send web notification to Topic instead of sending single notification to each token https://firebase.google.com/docs/cloud-messaging/js/topic-messaging

    public static bool SendFirebaseTopicNotificationAsync(string topicSubscription, NotificationPayload dat)
    {
        try
        {
            List<string> scopes = new List<string>();
            scopes.Add("https://www.googleapis.com/auth/firebase");
            scopes.Add("https://www.googleapis.com/auth/firebase.messaging");

            string accesstoken = GoogleCredential.FromFile(ConfigurationManager.AppSettings["GoogleCredentialFilePath"])
                                                        .CreateScoped(scopes)
                                                        .UnderlyingCredential
                                                        .GetAccessTokenForRequestAsync().Result;

            WebRequest tRequest = WebRequest.Create("https://fcm.googleapis.com/v1/projects/test-project/messages:send");
            tRequest.Method = "post";


            tRequest.Headers.Add("Authorization", "bearer " + accesstoken);
            tRequest.ContentType = "application/json";

            var payload = new
            {
                message = new
                {
                    topic = topicSubscription,
                    notification = new {
                        title = dat.title,
                        body = dat.body
                    },
                    webpush = new
                    {
                        fcm_options = new {
                            link = dat.click_action
                        }
                    }
                }
            };

            string postbody = JsonConvert.SerializeObject(payload).ToString();
            Byte[] byteArray = Encoding.UTF8.GetBytes(postbody);
            tRequest.ContentLength = byteArray.Length;
            using (Stream dataStream = tRequest.GetRequestStream())
            {
                dataStream.Write(byteArray, 0, byteArray.Length);
                using (WebResponse tResponse = tRequest.GetResponse())
                {
                    using (Stream dataStreamResponse = tResponse.GetResponseStream())
                    {
                        if (dataStreamResponse != null) using (StreamReader tReader = new StreamReader(dataStreamResponse))
                            {
                                String sResponseFromServer = tReader.ReadToEnd();
                                Console.WriteLine(sResponseFromServer);

                                return true;
                            }
                    }
                }
            }
        }
        catch (Exception ex)
        {
            log.Debug(ex.Message, ex);
        }

        return false;
    }

Note that test-project in SendFirebaseTopicNotificationAsync is project name. I'm sure it is true

ConfigurationManager.AppSettings["GoogleCredentialFilePath"] is absolute file path to .json key file which I'm following this guideline to retrieve https://www.c-sharpcorner.com/article/retrieve-access-token-for-google-service-account-form-json-or-p12-key-in-c-sharp/

But I get 401 Unauthorized error. It seems access token from GoogleCredential is wrong

Any helps will be appreciated

Thank you

1

1 Answers

0
votes

Stupid mistake here is this line

    tRequest.Headers.Add("Authorization", "bearer " + accesstoken);

bearer is wrong, use Bearer instead

    tRequest.Headers.Add("Authorization", "Bearer " + accesstoken);

I also found this article https://github.com/googleapis/google-api-dotnet-client/issues/1140

This guys also had 401 Unauthorized too. But his problem differs from me. He didn't enable FCM API in Google Cloud Platform https://console.cloud.google.com/apis/dashboard