Making user activation consistent across APIs

Mustaq Ahmed
Joe Medley
Joe Medley

To prevent malicious scripts from abusing sensitive APIs like popups, fullscreen etc., browsers control access to those APIs through user activation. User activation is the state of a browsing session with respect to user actions: an "active" state typically implies either the user is currently interacting with the page, or has completed an interaction since page load. User gesture is a popular but misleading term for the same idea. For example, a swipe or flick gesture by a user does not activate a page and hence is not, from a script standpoint, a user activation.

Major browsers today show widely divergent behavior around how user activation controls the activation-gated APIs. In Chrome, the implementation was based on a token-based model that turned out to be too complex to define a consistent behavior across all activation-gated APIs. For example, Chrome has been allowing incomplete access to activation-gated APIs through postMessage() and setTimeout() calls; and user activation wasn't supported with Promises, XHR, Gamepad interaction, etc. Note that some of these are popular yet long-standing bugs.

In version 72, Chrome ships User Activation v2 which makes user activation availability complete for all activation-gated APIs. This resolves the inconsistencies mentioned above (and a few more, like MessageChannels), which we believe would ease web development around user activation. Moreover, the new implementation provides a reference implementation for a proposed new specification that aims to bring all browsers together in the long run.

How does User Activation v2 work?

The new API maintains a two-bit user activation state at every window object in the frame hierarchy: a sticky bit for historical user activation state (if a frame has ever seen a user activation), and a transient bit for current state (if a frame has seen a user activation in about a second). The sticky bit never resets during the frame's lifetime after it gets set. The transient bit gets set on every user interaction, and is reset either after an expiry interval (about a second) or through a call to an activation-consuming API (e.g. window.open()).

Note that different activation-gated APIs rely on user activation in different ways; the new API is not changing any of these API-specific behaviors. E.g. only one popup is allowed per user activation because window.open() consumes user activation as it used to be, Navigator.prototype.vibrate() continues to be effective if a frame (or any of its subframes) has ever seen user action, and so on.

What's changing?

  • User Activation v2 formalizes the notion of user activation visibility across frame boundaries: a user interaction with a particular frame will now activate all containing frames (and only those frames) regardless of their origin. (In Chrome 72, we have a temporary workaround in place to expand the visibility to all same-origin frames. We will remove this workaround once we have a way to explicitly pass user activation to sub-frames.)
  • When an activation-gated API is called from an activated frame but from outside an event handler code, it will work as long as the user activation state is "active" (e.g. has neither expired nor been consumed). Before User Activation v2, it would unconditionally fail.
  • Multiple unused user interactions within the expiry time interval fuses into a single activation corresponding to the last interaction.

Examples of consistency in activation-gated APIs

Here are two examples with popup windows (opened using window.open()) that show how User Activation v2 makes the behavior of activation-gated APIs consistent.

Chained setTimeout() calls

This example is from our setTimeout() demo. If a click handler tries to open a popup within a second, it is expected to succeed no matter how the code "composes" the delay. User Activation v2 meets this expectation, so each of the following event handlers opens a popup on a click (with a 100ms delay):

function popupAfter100ms() {
  setTimeout(callWindowOpen, 100);
}

function asyncPopupAfter100ms() {
  setTimeout(popupAfter100ms, 0);
}

someButton.addEventListener('click', popupAfter100ms);
someButton.addEventListener('click', asyncPopupAfter100ms);

Without User Activation v2, the second event handler fails in all browsers we tested. (Even the first one fails in some cases.)

Cross-domain postMessage() calls

Here's an example from our postMessage() demo. Suppose a click handler in a cross-origin subframe sends two messages directly to the parent frame. The parent frame should be able to open a popup upon receiving either of these messages (but not both):

// Parent frame code
window.addEventListener('message', e => {
  if (e.data === 'open_popup' && e.origin === child_origin)
    window.open('about:blank');
});

// Child frame code:
someButton.addEventListener('click', () => {
  parent.postMessage('hi_there', parent_origin);
  parent.postMessage('open_popup', parent_origin);
});

Without User Activation v2, the parent frame can't open a popup upon receiving the second message. Even the first message fails if it is "chained" to another cross-origin frame (in other words, if the first receiver forwards the message to another).

This works with User Activation v2, both in the original form and with the chaining.