From 400f9fa95f9cd1d8e64e8217cf27e7bfa8361112 Mon Sep 17 00:00:00 2001 From: Shankari Date: Mon, 16 Dec 2024 14:03:47 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=82=20Login=20to=20the=20FCM=20mapping?= =?UTF-8?q?=20service=20using=20OAuth2=20tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FCM is doubling down on the "I'm going to change my API and break everything" approach. We made one round of fixes in: https://github.com/e-mission/e-mission-docs/issues/1094#issuecomment-2395599699 at which time the mapping to convert APNS tokens to FCM was working However, in the ~ 2 months since, that has also regressed, and we are now getting a 401 error with the old code. The new requirements include: - using an OAuth2 token instead of the server API key - passing in `"access_token_auth": "true"` as a header We already use an OAuth2 token to log in and actually send the messages ``` DEBUG:google.auth.transport.requests:Making request: POST https://oauth2.googleapis.com/token DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): oauth2.googleapis.com:443 DEBUG:urllib3.connectionpool:https://oauth2.googleapis.com:443 "POST /token HTTP/1.1" 200 None DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): fcm.googleapis.com:443 ``` So it seems like it would be best to just reuse it for this call as well. However, that token is retrieved from within the pyfcm library and is not easily exposed outside the library. Instead of retrieving the token, this change retrieves the entire authorization header. This header includes the token, but is also formatted correctly with the `Bearer` prefix and is accessible through the `requests_session` property. With this change, the mapping is successful and both silent and visible push notification are sent to iOS phones. Before the change: ``` DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): iid.googleapis.com:443 DEBUG:urllib3.connectionpool:https://iid.googleapis.com:443 "POST /iid/v1:batchImport HTTP/1.1" 401 None DEBUG:root:Response = Received invalid result for batch starting at = 0 after mapping iOS tokens, imported 0 -> processed 0 ``` After the change ``` DEBUG:root:Reading existing headers from current session {'User-Agent': 'python-requests/2.28.2', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'application/json', 'Authorization': 'Bearer ...'} DEBUG:root:About to send message {'application': 'gov.nrel.cims.openpath', 'sandbox': False, 'apns_tokens': [.... DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): iid.googleapis.com:443 DEBUG:urllib3.connectionpool:https://iid.googleapis.com:443 "POST /iid/v1:batchImport HTTP/1.1" 200 None DEBUG:root:Response = DEBUG:root:Found firebase mapping from ... at index 0 DEBUG:root:Found firebase mapping from ... at index 1 DEBUG:root:Found firebase mapping from ... at index 2 ... ``` Visible push ``` ... s see if the fix actually worked" -e nrelop_open-access_default_1hITb1CUmGT4iNqUgnifhDreySbQUrtP WARNING:root:Push configured for app gov.nrel.cims.openpath using platform firebase with token AAAAsojuOg... of length 152 after mapping iOS tokens, imported 0 -> processed 0 combo token map has 1 ios entries and 0 android entries {'success': 0, 'failure': 0, 'results': {}} Successfully sent to cK0jHHKUjS... {'success': 1, 'failure': 0, 'results': {'cK0jHHKUjS': 'projects/nrel-openpath/messages/1734384976007500'}} ``` --- .../push/notify_interface_impl/firebase.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/emission/net/ext_service/push/notify_interface_impl/firebase.py b/emission/net/ext_service/push/notify_interface_impl/firebase.py index d0b0671f2..4e6bc1b92 100644 --- a/emission/net/ext_service/push/notify_interface_impl/firebase.py +++ b/emission/net/ext_service/push/notify_interface_impl/firebase.py @@ -91,12 +91,17 @@ def map_existing_fcm_tokens(self, token_map): unmapped_token_list.append(token) return (mapped_token_map, unmapped_token_list) - def retrieve_fcm_tokens(self, token_list, dev): + def retrieve_fcm_tokens(self, push_service, token_list, dev): if len(token_list) == 0: logging.debug("len(token_list) == 0, skipping fcm token mapping to save API call") return [] importedResultList = [] - importHeaders = {"Authorization": "key=%s" % self.server_auth_token, + existing_headers = push_service.requests_session.headers + logging.debug(f"Reading existing headers from current session {existing_headers}") + # Copying over the authorization from existing headers since, as of Dec + # 2024, we cannot use the server API key and must use an OAuth2 token instead + importHeaders = {"Authorization": existing_headers['Authorization'], + "access_token_auth": "true", "Content-Type": "application/json"} for curr_first in range(0, len(token_list), 100): curr_batch = token_list[curr_first:curr_first + 100] @@ -115,7 +120,7 @@ def retrieve_fcm_tokens(self, token_list, dev): print("After appending result of size %s, total size = %s" % (len(importedResult), len(importedResultList))) else: - print(f"Received invalid result for batch starting at = {curr_first}") + print(f"Received invalid response {importResponse} for batch starting at = {curr_first}") return importedResultList def process_fcm_token_result(self, importedResultList): @@ -133,9 +138,9 @@ def process_fcm_token_result(self, importedResultList): (result, i)); return ret_list - def convert_to_fcm_if_necessary(self, token_map, dev): + def convert_to_fcm_if_necessary(self, push_service, token_map, dev): (mapped_token_map, unmapped_token_list) = self.map_existing_fcm_tokens(token_map) - importedResultList = self.retrieve_fcm_tokens(unmapped_token_list, dev) + importedResultList = self.retrieve_fcm_tokens(push_service, unmapped_token_list, dev) newly_mapped_token_list = self.process_fcm_token_result(importedResultList) print("after mapping iOS tokens, imported %s -> processed %s" % (len(importedResultList), len(newly_mapped_token_list))) @@ -152,15 +157,15 @@ def send_visible_notification(self, token_map, title, message, json_data, dev=Fa logging.info("len(token_map) == 0, early return to save api calls") return - # convert tokens if necessary - fcm_token_map = self.convert_to_fcm_if_necessary(token_map, dev) - push_service = FCMNotification( service_account_file=self.service_account_file, project_id=self.project_id) # Send android and iOS messages separately because they have slightly # different formats # https://github.com/e-mission/e-mission-server/issues/564#issuecomment-360720598 + # convert tokens if necessary + fcm_token_map = self.convert_to_fcm_if_necessary(push_service, token_map, dev) + android_response = self.notify_multiple_devices(push_service, fcm_token_map["android"], notification_body = message, @@ -192,7 +197,7 @@ def send_silent_notification(self, token_map, json_data, dev=False): project_id=self.project_id) # convert tokens if necessary - fcm_token_map = self.convert_to_fcm_if_necessary(token_map, dev) + fcm_token_map = self.convert_to_fcm_if_necessary(push_service, token_map, dev) response = {} response["ios"] = self.notify_multiple_devices(push_service,