Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Declarative Web Push #360

Open
beidson opened this issue Sep 11, 2023 · 20 comments
Open

Declarative Web Push #360

beidson opened this issue Sep 11, 2023 · 20 comments

Comments

@beidson
Copy link

beidson commented Sep 11, 2023

Declarative Web Push:

More efficient and privacy-preserving push notifications

By: Brady Eidson
Technical review by: Marcos Caceres and Anne van Kesteren.

This explainer proposes various changes to existing web standards so that, in the majority of cases, push-initiated notifications can be presented by the platform or user agent without involving a Service Worker.

The primary mechanism to enable this is by making the push message payload a declarative description of the notification to be displayed.

Our proposed model doesn’t require a Service Worker work to get a PushSubscription. Even if a Service Worker is registered, push notification messages bypass it by default.
This makes push notifications from the web more privacy-preserving as sites aren’t given the opportunity to execute any JavaScript until a user explicitly interacts with a notification. It also makes web push notifications more efficient by skipping the CPU and battery cost required to launch a Service Worker and execute its code.

We also propose a more flexible event handling model that developers can opt into to transform a push notification. This new model still depends on Service Workers, but provides the privacy-preserving guarantee that there is always a fallback notification to be shown, so pushes cannot trigger silent background runtime.

This explainer describes what we have implemented internally to prototype or what we plan to implement next.
Implementation experience and feedback from the standards community might change some details.

What are the drawbacks of current push notifications?

The W3C’s Push API and related specifications work great for maximizing user engagement, but suffer from notable drawbacks:

  • Requires a Service Worker which has higher web developer complexity, and which cost significant CPU, battery, and potentially network resources to present a notification.
  • Because a Service Worker is required, user agents with certain types of privacy features (such as Safari’s Intelligent Tracking Prevention) need to make hard trade-offs between push reliability and effective privacy.
  • User agents require a fallback plan for when the web app doesn’t fulfill their promise of displaying a user visible notification.

Learning from platform experience

Push notifications that come in to an OS platform are usually displayed by the platform itself, without any app code executing.
It is also fairly standard practice for apps to optionally get a small amount of CPU time to transform an incoming push notification - specially where changes since the last push message matter. Some examples of tasks performed during these allocated time slices include decrypting push data, updating the badge count, or updating based on score from an in-progress game.
If the app fails to complete a transformation task in their allotted time - due to coding error or other factors outside their control - then the original notification is displayed to a user. This automatic recovery model avoids disrupting the user experience, and ensures that push notifications don’t become a vector for untrusted silent background runtime.

We have deep experience with this flow and it is the base model for our proposed enhancements to Web Push.
Most existing web app that rely on the existing Service Workers driven-model can easily adapt to the more efficient and privacy preserving model; the web application can transform the received notification payload as needed, but the payload itself describes the primary notification. If a web app fails to complete the transformation in its allotted time, or there is a script error, a notification is still always presented to the end user.
When existing web apps simply include the full notification itself in push message JSON and have their service worker display it as-is, adopting the 100% streamlined model is trivial, and they can remove their push-related Service Worker code.

We detail how this model works in the sections below.

Goals

  • Specify a push + notification model that makes it optional to involve a service worker.
  • Specify a workflow that always results in a notification being presented to an end-user, even in the case of a script error.
  • Standardize a JSON representation of a Notification.
  • Retain full backwards compatibility with current Push API and associated standards.
  • Maximize reuse of push subscription infrastructure from Push API.
  • Support defaulting routing of notification actions using URLs instead of relying on the service worker.
  • Enable setting application badges through this mechanism.

Plan of action: amendments to existing specifications

The current set of specifications that govern the delivery and presentation of push notifications serve as a solid foundation to build on. They’ve served us well for a number of years, proving their robustness at Web scale.

To help with the standardization process and re-use existing push-related primitives and algorithms, we need to amend existing specs to support the new model we propose in this document.

The standards community would need to coordinate on small-to-moderate changes to RFC 8030, Push API, Notifications API, and possibly the Badging API specification. Our goal is to retain full backwards compatibility with the current Push API. Any amendments will live along side it (or integrate directly into the appropriate sections).

The spec changes proposed below are the ones we deemed to be the necessary starting points, but are by no means exhaustive. We are under no illusion that once we begin diving deeper more changes will be required.

RFC 8030 - Generic Event Delivery Using HTTP Push

RFC 8030 defines the mechanism by which push servers send messages to user agents. “Section 5 - Requesting Push Message Delivery” describes how a server requests that a push service send a push message payload to the user agent.

As far as HTTP is concerned, the push message data is just a blob posted in the HTTP request. But the HTTP request itself is where we can determine if the push message is “legacy” or “new” with an HTTP request header.

We propose that - even though it hasn’t been standard practice so far - RFC 8030 (or a follow-on spec) suggest push requests include a Content-Type. Most content type values will be ignored, with the payload understood to be a legacy push payload. A specific content type value will flag a message as containing a notification payload.

Later in this document we’ll detail a standard JSON format describing a visible notification payload, so one might think application/json makes sense. It also happens that legacy push message payloads are usually also JSON. And in our experience many legacy push message payloads are already sent with a Content-Type: application/json header.

To eliminate confusion between “legacy push JSON” and “declarative web push JSON”, we propose registering a new application/notification+json type.

Push messages received without that content-type are considered to have “legacy” disposition.
Push messages received with that content-type are considered to have a “notification” disposition, and the meaning of that will be covered later in this document.

Push API - Push Subscription

The Push API does a great job at describing a Push Subscription. However, as currently specified, it is intrinsically tied to a "Service Worker Registration". For one example:

“Each push subscription is associated with a service worker registration and a service worker registration has at most one push subscription”

Since a stated goal of this proposal is to work without a service worker, Push Subscription needs to be generalized and not bound to a service worker. We propose defining a push subscription owner, which can be either a “service worker registration” or security origin bound.

After that’s established, appropriately replace all references of “service worker registration as owner of a push subscription” with that of “push subscription owner”.

We also expose a navigator.pushManager on window.navigator, so that PushManager instances can be reached without needing a service worker registration.

Push subscriptions are interchangable. An existing push subscription that was made via a ServiceWorkerRegistration whose scope happens to match the security origin of a window object will be visible to that window.navigator.pushManager

Conversely, a new push subscription made via window.navigator.pushManager will be visible to a ServiceWorkerRegistration whose scope matches that security origin. Removing the subscription from one will be reflected in the other.

Push messages sent to that subscription can other have the legacy disposition and require a Service Worker to handle them with a push event, or have the notification disposition to allow for automatic handling, or an optional pass through a pushnotification event handler (described below)

Push API - subscribe() and related methods

For push subscriptions to be owned by something other than service worker registrations, and for a PushManager instance to be useful without having a service worker registration, PushManager.subscribe() will require an overhaul:

  • The subscribe() algorithm is heavily rooted in service worker registrations.
  • We propose forking the algorithm based on whether the global object is a service worker registration or a Window object.
  • The window object version will follow the same steps in principle.
  • getSubscription() and permissionState() will both need the same changes.

Push API - Receiving a push message

10.4 Receiving a Push Message will need a significant rewrite:

  • Don’t look up the Service Worker registration as the first step.
  • Instead inspect the Content-Type header to establish the payload’s disposition.
    • If the disposition is “legacy”, follow existing legacy web push steps starting at looking up the Service Worker registration
    • If the disposition is “notification” then:
      • Validate its JSON structure (see below)
      • If the JSON does not opt in to event handling through a Service Worker, then the user agent/platform just shows the notification directly.
      • If the JSON does opt in to event handling through a Service Worker:
        • Verify there is a registered service worker.
        • If not, abort these steps and display the notification directly like above.
        • If so:
          • Create or reuse an instance of the Service Worker
          • Fire a new event called pushnotification at it, whose event type includes a proposed Notification object
          • That handler has a small amount of runtime to call showNotification like legacy push event handlers do and then resolve the event. If it fails to do so, the JSON-derived Notification is presented as a fallback

We’re considering various ways that pushnotification events should specify their replacement Notification. Currently, calling showNotification like in the legacy case seems appropriate, but it’s a bit more complicated than it seems on the surface. We’ll share more thoughts once we figure them out.

In the notification disposition cases, the push message data is parsed as JSON and validated with certain requirements, such as providing a title, a default action, and the optional NotificationsOptions details. If the JSON doesn’t represent a well defined Notification object, it’s dropped on the floor (perhaps with a developer console warning).

To further enable the goal of avoiding the Service Worker when possible, we are also considering stricter requirements on service workers used for push event handling by keeping track of what event handlers are installed simply as a result of evaluating the Service Worker source code. For example, if synchronous evaluation of a Service Worker source doesn’t result in a pushnotification event handler being installed, then we would remember that and never consider firing a pushnotification event to it even if the notification payload JSON opts in.

We believe dynamically adding push event handlers to a Service Worker after initial evaluation is a developer error, and should not hurt platform performance, and making this mistake should result in a developer console warning, and not reduced performance or privacy characteristics for the user.

Activating a notification

In legacy Web Push, when a notification is activated, the user agent handles it by dispatching a notificationclick event at a Service Worker instance. Usually that event handler verifies a window client exists at an app specified URL to send it a message, or opens a new window client to an app-specified URL.

Navigating the user agent to HTTP URLs is the native language of the web platform, and opening a URL is the most common result of processing a notificationclick event. So our proposed model uses URLs as a declarative means to serve the same purpose.

Therefore a requirement of the notification payload JSON is to specify a “default action URL”, and NotificationAction will be extended to also have an “action URL”. Any NotificationActions specified in the NotificationOptions JSON will be required to specify an action URL.

We propose extending the JavaScript API for creating a persistent notification - ServiceWorkerRegistration.showNotification() - to allow for optionally specifying action URLs. Both the default action URL, and the NotificationOptions NotificationAction action URLs.

If a persistent notification is activated with an action that has an associated action URL, notificationclick event dispatch can be skipped and the URL is opened directly.

Therefore, persistent notifications created from a notification disposition payload will aways skip the notificationclick handler, and legacy push implementations that create notifications with Service Workers can optionally skip the notificationclick handler.

Notifications API

Legacy Push API gives the entire push message data to the web application, to be used however it sees fit. As established earlier, our proposal leverages a standardized JSON structure to represent that notification and related data.

The Notifications API needs a few tweaks to reflect all desired new behaviors:

  • The concept of “persistent notification” needs to change from “a notification with an associated service worker registration” to a notification with an associated service worker registration or associated push subscription”.
    Or some other language that clarifies automatically created Notifications from a notification push payload also qualify as “persistent”
  • Automatically created Notification objects are defined to have the associated push subscription.
  • Replacement Notification objects created during pushnotification event handling also have the associated push subscription.
  • Where the window security origin exactly matches with a service worker scope, both entities are candidates for handling a given push message depending on its details.
  • NotificationAction specifiers created in JavaScript need to include an optional action URL.
  • Similarly, creating a persistent notification in JavaScript also needs a way to specify an option default action URL, probably in NotificationOptions
  • The Notifications spec will cover the JSON expected in a push notification payload.

In our current proof of concept implementation, we implement JSON structure illustrated by the following example:

{
  "title": "The same as the 'title' in existing Notification JS APIs",
  "options": {
    "body": "most of the 'options' entries are directly from NotificationOptions",
    "lang": "like this malformed 'lang' tag",
    "dir": "LTR",
    "silent": "true",
    "actions": [
      {
        "action": "confirm",
        "title": "Confirm",
        "url": "https://webkit.org/confirm.html"
      },
      {
        "action": "deny",
        "title": "Deny",
        "url": "https://webkit.org/deny.html"
      }
    ],
    "data": {
      "this": "section is freeform",
      "normally": "the data part of a NotificationOptions",
      "dictionary": "is any serializable JavaScript object",
      "but": "when specified in a push notification payload",
      "it": "is standard JSON that will parsed into a JS object as needed"
    }
  },
  "default_action_url": "https://webkit.org/blog",
  "mutable": true,
  "app_badge": 17
}

The top level “title” member is required, and represents the title that would be passed in to e.g. the Notification constructor.
The “options” member is optional, and represents the NotificationOptions dictionary that would be passed in to e.g. the Notification constructor.

  • The "default_action_url" member is required, and represents the url that the user agent should load when the notification is activated.
  • Similarly, if any actions are specified within “options”, notice that each of them is required to have a “url” entry. If that specific action is selected, then its URL will be loaded.
  • The "mutable" field is optional. Notification payloads are immutable by default (false). An “immutable” notification payload will always be displayed directly by the platform or user agent, even if a service worker registration matches and has a pushnotification event handler.
  • A “mutable” notification payload is eligible to be transformed by a pushnotification event handler, if a handler has been preregistered.
  • “app_badge” is optional. If specified must be an integer in the unsigned long long range. If specified as a positive number, then the set the app badge steps from the Badging API are taken. Similarly, if the value is 0, the badge is cleared as per set the application badge steps.

The inclusion of badging raises a few as-of-yet unanswered questions that we don’t have strong thoughts about quite yet, such as:

  • Do we allow a notification payload whose only member is “app_badge”, skipping the notification but allowing efficient updating of the application badge?
  • If "app_badge" is included in a mutable notification payload, how do we include the passed in app_badge value to the pushnotification event? And how do we allow the event handler to transform the resulting app badge?

These questions do need answers, but we’re not letting them hold up progress in implementing the rest of the proposal.

In this description of the JSON fields, we’ve mentioned some optional and some required fields. We’ve also mentioned some acceptable value ranges for certain fields.

Requiring validation of the the incoming JSON to the extent that it needed to fully describe a Notification that the platform or user agent can handle directly is one of the goals of declarative web push. If the JSON fails to meet those requirements, then it will be ignored, and likely an error message shown to the developer console.

Finally, since concepts and algothrim steps from Notifications API are being borrowed and repurposed to support declarative, automatic handling by the User Agent, as opposed to imperative API calls, we might want to rename the Notifications API to just “Notifications”

Badging API specification

In the above description of the JSON payload we mention executing steps in the Badging API spec.

The algorithms of the badging specification need to be generalized so, where possible, they are not tied to the API. The solution we come up with as part of this process needs to work with the existing values and model used by the badging specification. As such, we would probably want to generalize and rename the Badging API to just “Badging”.

@martinthomson
Copy link
Member

(preface: I have not read this very long message in its entirety yet)

I'm supportive of this general thrust, but I want to ensure that we do this well. Revisions to IETF protocols probably need to happen in the IETF. I also think that there is an opportunity to fix some issues with the cryptography used by Web Push at the time that we look at new content types.

@othermaciej
Copy link
Member

Tentatively, I think the only IETF level change required here is to encourage push requests to have a Content-Type and specify that JSON content types have special meaning.

Defining the format for application/notification+json and registering the content type can be done in W3C space I think, since the IETF protocol is otherwise payload-agnostic. The proposed changes to Push API and Notification API would go here and in https://github.com/whatwg/notifications respectively. In due course issues should be raised in all three places but this seems like a good starting point for reviewing the overall plan before taking that step.

@martinthomson
Copy link
Member

It is absolutely not secure to change the content type, which is not authenticated, and reinterpret the content of a message (which is raw binary) differently as a result. That creates what could turn into attacks on sites.

That is why I think that we will have to make at least some revisions to the format of messages.

@tomayac
Copy link

tomayac commented Sep 15, 2023

Would PushManager also be exposed in a WorkerNavigator context?

@tomayac
Copy link

tomayac commented Sep 15, 2023

The current NotificationOptions use camelCase (like requireInteraction). This proposal should probably align.

@tomayac
Copy link

tomayac commented Sep 15, 2023

The current NotificationOptions already include badge. There's a potential confusion with your proposed appBadge (camelCased your original app_badge, as per my suggestion in #360 (comment)). I do reckon that appBadge is a perfect name given the intended use case, though, so the potential for confusion might just be something to live with.

@tomayac
Copy link

tomayac commented Sep 15, 2023

Similar to your open question…

Do we allow a notification payload whose only member is "app_badge", skipping the notification but allowing efficient updating of the application badge?

…should this allow notification payloads with just "data"? The use case would be apps that keep settings like, say, the chosen app language purely on the client. For example, for an airline travel app, this would allow the server to send a notification with a "data" object like…

{
  "data": {
    "type": "gate_change",
    "details": {
      "old": 12,
      "new": 25
    }
  }
}

…that the app could then dynamically expand to a notification like "There's a gate change for your flight, the old gate was 12, the new gate is now 25" when the app language is set to English, and translations thereof for other languages. This of course requires allowing script execution, so may be out of scope slightly.

@beidson
Copy link
Author

beidson commented Oct 18, 2023

Would PushManager also be exposed in a WorkerNavigator context?

That would introduce a situation where - in a SW context - you have two ways to get to the PushManager. Which is... not necessarily problematic but... is weird.

The use case isn't as compelling, but it could be?

@beidson
Copy link
Author

beidson commented Oct 18, 2023

The current NotificationOptions already include badge. There's a potential confusion with your proposed appBadge (camelCased your original app_badge, as per my suggestion in #360 (comment)). I do reckon that appBadge is a perfect name given the intended use case, though, so the potential for confusion might just be something to live with.

We discussed this.

Putting appBadge on NotificationOptions is necessary, and we can't change the old badge for backwards compatibility.

There is potential for confusion, but the typing (string vs. uint64_t) and docs will help.

@beidson
Copy link
Author

beidson commented Oct 18, 2023

…should this allow notification payloads with just "data"? The use case would be apps that keep settings like, say, the chosen app language purely on the client. For example, for an airline travel app, this would allow the server to send a notification with a "data" object like…

I don't think so.

Because as you mention:

This of course requires allowing script execution, so may be out of scope slightly.

The primary goal here is that a payload always represents a user visible action the platform or user agent can take without javascript.

A "data only" message that's immutable would, of course, be a no-op.
A "data only" message that's mutable could be processed by SW javascript, but doesn't have the user visible fallback that is required by the proposal. e.g. if the JS failed to display a notification, the UA wouldn't have the default message to display.

A "data only message that absolutely requires JS to display the notification" is actually a precise description of existing web push, which lives side-by-side with the declarative model.

@beidson
Copy link
Author

beidson commented Oct 18, 2023

The current NotificationOptions use camelCase (like requireInteraction). This proposal should probably align.

Between when this explainer was written and when I landed our first implementation, I believe I made this change already.

Will be updating the explainer sometime soon.

@tomayac
Copy link

tomayac commented Oct 19, 2023

Thanks for answering the other questions, I'm all 👍 with your answers. Also agree for the "data"-only use case to not work.

That would introduce a situation where - in a SW context - you have two ways to get to the PushManager. Which is... not necessarily problematic but... is weird.

From the Explainer prose:

Push subscriptions are interchang[e]able. An existing push subscription that was made via a ServiceWorkerRegistration whose scope happens to match the security origin of a window object will be visible to that window.navigator.pushManager

Conversely, a new push subscription made via window.navigator.pushManager will be visible to a ServiceWorkerRegistration whose scope matches that security origin. Removing the subscription from one will be reflected in the other.

There's some interchangeability built into the proposal already, so having two ways of accessing the push manager doesn't seem overly surprising to me at least.

The use case isn't as compelling, but it could be?

It's mostly just a question I had. Not sure if there would be user demand.

@saschanaz
Copy link
Member

saschanaz commented Feb 13, 2024

The concept of “persistent notification” needs to change from “a notification with an associated service worker registration” to a notification with an associated service worker registration or associated push subscription”.
Or some other language that clarifies automatically created Notifications from a notification push payload also qualify as “persistent”

Can this be "a notification with default action URL"? Such notification would have an action to do regardless of SW or push subscription, and thus can exist in system notification center even after the browser exits (meaning, "persistent"), right?

Edit: Now this is being done whatwg/notifications#213

@saschanaz
Copy link
Member

And with that I wonder we can introduce a persistent notification API that can be used regardless of push and SW, and then we can try this proposal with two steps: handling push and handling notification click, each without triggering SW.

It could be enough to add a default action URL option to existing new Notification(). Thoughts?

@annevk
Copy link
Member

annevk commented Oct 24, 2024

Status update:

We will be updating our implementation in WebKit to reflect the above.

annevk added a commit to whatwg/notifications that referenced this issue Nov 7, 2024
This makes notifications more declarative by not requiring explicit handling of the clicks by the web application.

This is part of the Declarative Web Push effort: w3c/push-api#360.
@jrconlin
Copy link

I have a few general questions about this, mostly around the payload element:

  1. Would proposed JSON Payload remain fully e2e encrypted or would there be elements of that payload that would not be encrypted (e.g. to allow for the "default" displayable message or action URL in case of encryption failures)? If so, how do we ensure message privacy? (e.g. Could a company that provides Push be subpoenaed to report all user information for messages related to a given "default URL"?)

  2. Would the default 4K message size limit be maintained with allowances for the cryptographic overhead, or would the message size be increased to handle the additional required fields?

  3. Companies like Mozilla rely on using proprietary messaging systems like Android Firebase Cloud Messaging (FCM) and Apple's Push Notification system (APNs) to deliver push messages to mobile platforms. These also have existing message size limits, which may impact the total available size of an encrypted message payload. How would those companies deal with this sort of issue (ideally, without implementing a "TCP over FCM/APNs" type solution)?

@annevk
Copy link
Member

annevk commented Nov 13, 2024

  1. It remains fully encrypted. (This should follow from the PR. We only parse the payload post decryption.) In case of encryption failures you'd have to do some kind of fallback or drop the message on the floor. Essentially whatever happens today for that case.
  2. This would remain the same. I suspect that changing this would require larger infrastructure changes and the idea here is for this to be a possible drop-in replacement of opaque messages.
  3. Given the answer to 2 I expect no impact here, but I might be missing something.

@jrconlin
Copy link

Thank you, those answers do address most of my initial concerns.

For what it's worth, we generally see fairly large payloads (although I suspect that's mostly due to padding). I can see how the structured form could impact the amount of data being exchanged, but I also expect that it might address some other security concerns.

I also wonder how or if this modification would see adoption unless there's a concerted effort to either promote it or some other incentive for publishers to adopt it? FWIW, even 8 years after the release of RFC 8030, and three years after a personal effort to have a majority of Push libraries change their default, half our subscription load still uses the older "aesgcm" encoding, indicating that A LOT of publishers never update their code. Hopefully, the "You don't need to copy/paste your service worker code", will provide enough incentive for those publishers, but I suspect that problem is out of scope of this proposal.

@annevk
Copy link
Member

annevk commented Nov 13, 2024

I think there's two main factors to drive adoption:

  1. Robustness. Even if the browser sees need to remove your service worker or not invoke it due to consuming-end-user-resources concerns, a notification can still be delivered as keeping around a push notification registration is much lower cost and showing a notification without involving a service worker is lower cost as well.
  2. Easier to adopt. If you don't need to create a service worker for push notifications, it will be easier to add push notifications to your website.

I think there's also some benefit that if you do enable service workers (through "mutable": true) you get a push event that contains much more structured data allowing you to simplify your client logic.

annevk added a commit that referenced this issue Nov 19, 2024
This is part of the Declarative Web Push initiative (see #360) and mainly makes sense when that is supported, although could be independently supported in theory.

This makes window.pushManager work by making push subscriptions tied to a scope rather than a service worker registration. Most often push subscriptions remain 1:1 with service worker registrations, but the scope whose serialized path is "/" is treated specially from now on and can exist independently.

This obsoletes #368.
@annevk
Copy link
Member

annevk commented Nov 19, 2024

All three major PRs in #360 (comment) are ready now. All that remains is some better integration with the Badging API and defining more precisely what signal user agents can use to determine whether to display the declarative notification/badge when mutable is used as requested by mt. Now would be a good moment to give feedback if any remains.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants