Mobile Push Notifications
Overview
Push notifications are a somewhat complex area, and their implementation differs both on the server and on the client, based on different dimensions.
From a server perspective, the implementation differs between the Bitwarden cloud-hosted instance and self-hosted instances. The primary difference is that self-hosted clients need to relay their messages through the Bitwarden cloud-hosted instance. This is needed since only Bitwarden is allowed to send notifications to the store-distributed Android and iOS mobile applications.
From a client perspective, the implementations differ between mobile operating systems, Android and iOS. This is because each operating system handles acquiring and refreshing push tokens differently.
We will first look at the server-side implementations, then look at acquiring push tokens on the clients.
Server Implementations
Sending the push token to Azure Notification Hub
The mobile client - whether iOS or Android - receives an opaque token that represents that physical
device to the platform-specific notification service. The client transmits this token to the server
through a POST
request to the /devices/identifier/{deviceIdentifier}/token
endpoint on the
Bitwarden API. On the mobile client this is done in the OnRegisteredAsync()
method of the
PushNotificationListenerService
.
The Bitwarden API is then responsible for submitting this token to the Azure Notification Hub. On
the server, the opaque push token is associated with a specific user via the user presented in the
access_token
on the request and a physical device from the URL path. This is stored in SQL in the
Device
table.
It is important to recognize that at this point we have associated a token with the combination of both a user and a physical device, as both of these are used as tags on the registration in Azure Notification Hub. This is how we ensure that subsequent notifications are only sent to the device when the appropriate user triggers them on another device.
Cloud implementation
If we are running a Bitwarden cloud instance, the Bitwarden API is responsible for directly
communicating with the Azure Notification Hub to register the push token. This is done in the
CreateOrUpdateRegistrationAsync()
method on the
NotificationHubPushRegistrationService
.
Self-hosted implementation
For self-hosted instances, the self-hosted instance cannot communicate directly with Bitwarden's
Azure Notification Hub. In order to provide push notifications for self-hosted instances, the
self-hosted Bitwarden API must register with the Azure Notification Hub through the
CreateOrUpdateRegistrationAsync()
method on
RelayPushRegistrationService
.
This implementation of
IPushRegistrationService
allows the self-hosted Bitwarden API to register the push token by calling the /push/register
endpoint on the
PushController
in the Bitwarden Cloud API. This is exposed to the self-hosted instance as
https://push.bitwarden.com. The
PushController
on the Bitwarden Cloud API then registers the push token as if it were a cloud registration -
sending it to Azure Notification Hub.
It is important to understand the change in context when moving through the relay push notification
service. The relay communicates between two different servers running the Bitwarden API - the
self-hosted instance and the Bitwarden Cloud instance. Each of these servers has different
implementations of IPushNotificationService. Once the message is received by the Bitwarden Cloud API
/push/register
endpoint, it is handled just like any other push notification triggered from the
service itself.
Using the push token to send notifications to the device
When a client changes data, or a Passwordless Authentication Request is sent, the server is responsible for sending push notifications to all mobile clients to make them aware of the change.
Cloud implementation
For notifications to mobile devices, this is handled in the
NotificationHubPushNotificationService
.
This service uses the Microsoft.Azure.NotificationHubs
SDK to send notifications to the Azure
Notification Hub.
When registering with Azure Notification Hub, each push token is associated with both a user and a device, as we saw above. It is at this point that these tags are used to target specific notifications. For each type of notification that the server wishes to send, it is tagged with the device identifier and user ID. Azure Notification Hub then uses those tags to look up the push token and send the notification to the proper device. This ensures that we only send notifications to a device when the user and device match.
Self-hosted implementation
Just as with the registration of the push token, the self-hosted instance uses the
PushController
on the Bitwarden Cloud API as a proxy to communicate with the Azure Notification Hub.
The self-hosted Bitwarden API calls the /send
endpoint on the PushController
on the Bitwarden
Cloud API to transmit the push payload to the Bitwarden Cloud API. The Cloud API then transmits the
data to the Azure Notification Hub using the same
NotificationHubPushNotificationService
as it would for a cloud-generated message.
It is important to note that from the Cloud API's perspective, it handles a message received from
the /send
endpoint the same way it does a message generated from an action on the Bitwarden Cloud
server; there is no difference and the same code is executed either way.
No decrypted data is ever sent in push notification payloads, and the data is never stored on the Bitwarden Cloud database when being proxied by the push relay. This allows our self-hosted instances to keep their data segregated from the Bitwarden Cloud and still use push notifications.
Client Registration
Obtaining push tokens on the mobile clients
The process for obtaining the opaque device push tokens on the mobile client varies based on the mobile OS.
Android
Android push tokens are received by the
FirebaseMessagingService
.
Firebase Cloud Messaging (FCM) is the Platform Notification Service used for push notifications to
Android devices. When the Android OS initially obtains a token for the application, or the token is
updated, the OnNewToken()
method in this service is triggered.
In the OnNewToken()
method, we update the PushRegisteredToken
in the device's state and trigger
the RegisterAsync()
method of the
AndroidPushNotificationService
.
Here, we use PushRegisteredToken
in state to represent the recent token received from FCM. It is
scoped to exist once for a given device, because FCM assigns push tokens to a given device,
regardless of Bitwarden accounts on the device.
At this point, the PushRegisteredToken
represents the token assigned to the device by FCM.
However, Bitwarden stores push tokens for each individual user on the device, in order to target the
notifications appropriately. In order to capture this level of granularity, we store the
PushCurrentToken
in state at the user level. The PushCurrentToken
represents the individual
user's push token, which may or may not be out of date with the one assigned by FCM.
It is the responsibility of RegisterSync()
on the
AndroidPushNotificationService
to determine if the PushRegisteredToken
assigned to the device differs from the PushCurrentToken
assigned to the current user.
If the current token assigned to the device differs from the user's token, we call
OnRegisteredSync()
on the
PushNotificationListenerService
to:
- Register the new
PushRegisteredToken
through the Bitwarden API for the active user - Set the
PushCurrentToken
to the new value for the active user
As we can see, the RegisterSync()
handles registering the push token for the active user only.
In the case that there are multiple users on the device, only the user active when FCM issues a new
token will get the update through the process described thus far.
For other users on the device, RegisterSync()
is initiated when they next log in to the
application or the application is switched to their account. This is done in the initialization of
the
GroupingsPage
.
The same comparison is done for this user, and in this case the PushRegisteredToken
is still
different than the PushCurrentToken
for that user (as we've only updated the PushCurrentToken
for the initial user thus far). It is at this point that the Bitwarden API is notified that the
subsequent users are registered for the new token.
The Android Push Notification documentation applies to the Bitwarden application that is installed
from the Google Play store. The FDroid release does not support push notifications. These builds use
the
NoopPushNotificationListenerService
and the
NoopPushNotificationService
.
iOS
On iOS devices, push token registration occurs through the Apple Push Notification service (APNs).
When a user logs in to the iOS application or switches accounts, the application loads the
GroupingsPage
.
In the
GroupingsPage
initialization, we first check to make sure the device has accepted push notifications. If not, the
Bitwarden push notification prompt is shown. This prompt explains why iOS will be requesting push
notifications for the Bitwarden mobile app.
If the user accepts this prompt, or if they already have accepted it, the application checks to see
if the current user has registered for push notifications within the last day. If they have never
registered before, or if more than one day has elapsed, the Bitwarden app registers with iOS for
push notifications, requesting a push token. This is done in the RegisterAsync()
method in
iOSPushNotificationService
.
RegisterAsync()
executes the iOS platform-specific method required to begin the token request
process from APNs.
The response from APNs with the push token is received asynchronously. When a token is obtained for
the device, the OnRegisteredSuccess()
method in
iOSPushNotificationHandler
is triggered. This then calls the OnRegisteredAsync()
method of the
PushNotificationListenerService
,
passing along the newly-acquired token. This method is responsible for sending the push token to the
back-end API to register the device + user combination for push notifications.
We are registering for a push token once a day for each account on the device. However, it is quite likely that the token received from iOS will be the same each day. A different token is generated for a device in some specific scenarios, like restoring device from a backup. Even though this is not usually the case, we check on a daily basis to ensure that the Bitwarden Azure Notification Hub is up to date.