Skip to content

Commit

Permalink
🛂 Login to the FCM mapping service using OAuth2 tokens
Browse files Browse the repository at this point in the history
FCM is doubling down on the "I'm going to change my API and break
everything" approach. We made one round of fixes in:
e-mission/e-mission-docs#1094 (comment)
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 = <Response [401]>
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 = <Response [200]>
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'}}

```
  • Loading branch information
shankari committed Dec 16, 2024
1 parent 23e60d5 commit 400f9fa
Showing 1 changed file with 14 additions and 9 deletions.
23 changes: 14 additions & 9 deletions emission/net/ext_service/push/notify_interface_impl/firebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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):
Expand All @@ -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)))
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 400f9fa

Please sign in to comment.