It's often the case that the web platform is compared to native platforms and found wanting when it comes to the variety and richness of available device APIs. This is frequently coupled with worries of an "infobarpocalypse"; the tendency for browsers to prompt users early-and-often when exposing sensitive APIs. The worry emerges that this ask-and-ye-shall-find model is confusing to both users and developers.
Thoughtful voices have likewise noted the downsides of up-front permission grants in application platforms like Windows (implicitly) and Android (explicitly). Upgrades to these applications become hostage-taking scenarios: if you like the app and want to keep using it, your only alternative is to grant it ever-more permission and access to sensitive data over time -- with no hope of revocation. User recourse is limited to app removal and the only ecosystem-level fix is through revocations via relatively-closed app stores.
How, then, can the web ever hope to expand to grant more capability, safely, without devolving into a high-friction world of app stores, packaging, and signing?
Review and classification of the permissions landscape of native ecosystems by Aaron Boodman, Rafael Weinstein, and Adrienne Porter-Felt of the Google Chrome team points to two broad classes of permissions: those that can operate within the Origin Model of web security and those which circumvent it.
For instance, a Raw Socket API that allowed only connections to be initiated to a web application's origin host & port is likely origin-preserving. A Raw Socket API that allowed connection to arbitrary hosts, or even multiple ports on the originating host, clearly breaks the origin model. For the purposes of the rest of this post, we'll consider only the origin preserving subset of capabilities.
Existing infobar-prompting permissions can be an answer to the up-front grant model, but as some have pointed out, it gets messy when applied as a general answer. Getting agreement between vendors about what UI is appropriate is a hill on which many previous efforts have died. Making seems incompatible with forging UA-level agreement about UI.
An important axis for considering these questions is permission management. Having APIs which enable users to revoke permissions later is the dual of asking users to grant permissions at runtime (not up-front).
Extracting this value for users requires that application developers must handle failure and that permission-requiring APIs be asynchronous.
The constraints previously outlined inform a pattern we have begun to adopt in the design of new features like Background Notifications, Background Synchronization, and Push Messaging. Extracting this pattern and describing it can improve overall platform consistency. We see this with Promises where existing APIs which previously took callbacks and returned nothing can be easily retrofitted to make callback arguments optional and return Promises. This enables them to work nicely along-side new platform APIs that support only the more modern Promise style.
The overall constraints on the design space are:
- Requests for permissions must be asynchronous
- It must be possible for a permission request to fail
- It must be possible to (asynchronously) detect if a permission is already available in order to enable adaptive UI
- Any general pattern should be complementary, or at least compatible, with existing asynchronous permission request APIs in the platform (to allow them to be retrofitted, as many callback APIs have been with Promises)
Lets examine an example API for a hypothetical permission that controls whether or not the site is allowed to frobulate:
// https://example.com/frobulator.js
// Boilerplate setup
var onerror = console.error.bind(console);
var frobulateButton = document.getElementById("frobulateButton");
// Feature detect
if (!navigator.requestFrobulatePermission) {
// This runtime doesn't have the feature, so hide the button or show an error
frobulateButton.style.display = "none";
} else {
// Set up the button to request permission in-context:
frobulateButton.onclick = function() {
navigator.hasFrobulatePermission().then(
function(disposition) {
// The "has*Permission" methods return "default", "granted", or "denied"
switch (disposition) {
case "default": // We haven't requested the permission...
case "granted": // ...or we know we have it, so request API access:
navigator.requestFrobulatePermission().then(
function(forbulateAPI) {
// No UI is shown to user if previously granted, whereas user
// may be prompted about frobulation if disposition is
// "default".
forbulateAPI.frobulate();
},
onerror
);
case "denied":
// Disable our UI for the action.
frobulateButton.disabled = true;
onerror("unable to frobulate; permission denied");
break;
}
}
);
}
}
Note that the app might also call navigator.hasFrobulatePermission()
at page load time to determine if the button should be enabled at all. This is a good way to provide the user with meaningful options.
The basic formula is:
- A
request*Permission()
function that asynchronously returns the actual API - A
has*Permission()
function that allows the app to determine the current state without creating prompt UI when users might not be expecting it. - The ability to feature-detect the presence of the API
This pattern can be applied broadly and is flexible both to the needs of developers but also those of UAs and the users they work to protect.
One possible concern with the hasFrobulatePemission()
method is that it potentially leaks a previous decision.
Because of the async nature of this API style, UAs can mitigate against this leakage (to the extent they think this is a problem) by:
- Only allowing calls to
has*Permission()
calls in response to user action (e.g., in the event handler for a non-synthetic click event). - Delaying the delivery of a value by a random time interval
- Providing users visibility and control over if and when sites are allowed to request this data through system management and revocation UI
In general, it is good practice for UAs to enable users to audit and revoke the set of information that sites are able to collect. The amount of ambient information available to sites at load time is frequently enough to enable fingerprinting when a UA is not in a specific privacy-preserving mode.
The API pattern presented can function correctly in such a mode, always returning "default"
to has*Permission()
calls should UAs choose to implement.
The above mitigation for privacy concerns is but one example of a very positive aspect of this emerging pattern: it allows UA's to make decisions asynchronously about when (and if) to show users UI for mediating various permission access.
Consider a situation in which a user interacts with a site frequently and the UA surfaces the use of frobulateAPI
in secure UI which allows users to audit and revoke the capability on a per-site basis. In such a situation, a UA might possibly forego ever asking the user for explicit permission via infobar if the UA has knowledge that the user trusts the site in other ways -- for instance if the user visits frequently, has added a bookmark to their homescreen, and if the permission is not particularly sensitive but may be annoying (e.g., Notification).
Given the recent history of system UIs for vending and managing these sorts of capabilities, there is little reason to think that UAs will coalesce around a single UI treatment for all permissions; or that these treatments will be static over time.
The flexibility to change UI, to surface system use and capabilities in context, and to enable content to react to user preferences meaningfully are advantages to this API style. Experimentation and iteration are the crux of healthy UI. We hope this pattern can enable that sort of experimentation and provide a path away from the infobarpocalypse; not by depriving users of control, but by enabling UAs to do better.
Questions from TAG discussion:
has*Permission()
API example shouldn't be guarded by user interaction