A Tour of the JavaScript Permissions API

Jack Misteli

If you have ever created a web application which requires different features (like push notifications, webcam access, midi access), you probably noticed that their APIs look very different.

// for the geolocation API you need to call getCurrentPosition to check if geolocation is accessible
navigator.geolocation.getCurrentPosition(gotLocation, didNotGetLocation);

// for notifications we can directly check on the Notification object

if (Notification.permission == 'granted')
  // do notification stuff
if (Notification.permission == 'denied')
  // ask for notification access ....

This is not very handy.

The Permissions API allows us to have overview of the permissions available on our pages. What we mean by “permission” is whether we can access a specific feature with our code. Features that require permission to access them with code are called powerful features. Camera, midi, notifications, geolocation are all powerful features.

All powerful feature’s APIs are a bit different. Thus, it can be a pain to figure out what is the state of each feature’s permissions. With the Permissions API, we manage all the permission’s statuses with a single interface.

Permissions API Basics

The permission API is very experimental at this stage and should be used carefully. You should only use it if it is mission critical and you can keep up with future breaking changes. For instance, a few browsers used to support navigator.permissions.revoke but it is now deprecated.

At the time of the writing, query is the only property we can access from then permissions interface. query takes an object as an argument called a PermissionDescriptor. The permission descriptor has one field called name, which is the name of the permission you want to access.

// This query will give us information about the permissions attached to the camera
navigator.permissions.query({name: 'camera'})

The query returns a promise which resolves to a PermissionStatus. PermissionStatus has two fields: state and onchange.

navigator.permissions.query({name: 'camera'}).then( permissionStatus => {
  console.log(permissionStatus)
  // in my browser on this page it logs:
  //{
  //   status: "prompt",
  //   onchange: null,
  // }
})

state has 3 possible states: “granted”, “denied” and “prompt”. “granted” means that we have access to the feature. “denied” means that we won’t be able to access the feature. “prompt” means that the User-Agent (i.e. the browser) will ask the user for permission if we try to access that feature.

Some PermissionDescriptor have additional fields and you can read more about them here. For example, camera’s PermissionDescriptor has an additional field called deviceId if you want to target a specific camera. Your query might look like this: .query({name: 'camera', deviceId: "my-device-id"}).

onchange is an event listener which activates whenever the permissions of the queried feature changes.

navigator.permissions.query({name:'camera'}).then(res => {
  res.onchange = ((e)=>{
    // detecting if the event is a change
    if (e.type === 'change'){
      // checking what the new permissionStatus state is
      const newState = e.target.state
      if (newState === 'denied') {
        console.log('why did you decide to block us?')
      } else if (newState === 'granted') {
        console.log('We will be together forever!')
      } else {
        console.log('Thanks for reverting things back to normal')
      }
    }
  })
})

All Permissions API

There are a lot of different powerful permissions and browser support is very uneven. In the following script, you can see all the permissions described by W3C’s editor’s draft in the permissionsName variable. The getAllPermissions function returns an array with the different permissions available and their state. Please note that the result will change depending on your browser, the user’s preference and of course the website’s setup.

const permissionsNames = [
  "geolocation",
  "notifications",
  "push",
  "midi",
  "camera",
  "microphone",
  "speaker",
  "device-info",
  "background-fetch",
  "background-sync",
  "bluetooth",
  "persistent-storage",
  "ambient-light-sensor",
  "accelerometer",
  "gyroscope",
  "magnetometer",
  "clipboard",
  "display-capture",
  "nfc"
]

const getAllPermissions = async () => {
  const allPermissions = []
  // We use Promise.all to wait until all the permission queries are resolved
  await Promise.all(
    permissionsNames.map(async permissionName => {
        try {
          let permission
          switch (permissionName) {
            case 'push':
              // Not necessary but right now Chrome only supports push messages with  notifications
              permission = await navigator.permissions.query({name: permissionName, userVisibleOnly: true})
              break
            default:
              permission = await navigator.permissions.query({name: permissionName})
          }
          console.log(permission)
          allPermissions.push({permissionName, state: permission.state})
        }
        catch(e){
          allPermissions.push({permissionName, state: 'error', errorMessage: e.toString()})
        }
    })
  )
  return allPermissions
}

If I then run the following code in my developer console on Alligator.io:

(async function () {
  const allPermissions = await getAllPermissions()
  console.log(allPermissions)
})()

Here’s a screenshot of what I get at the console:

Permissions in Workers

So far we only used the navigator.permissions API because it is much easier to write concise examples. The Permissions API is also available inside of workers. WorkerNavigator.permissions allows us to check permissions inside our workers.

Hopefully you now have a better idea on how to use the Permissions API. It's not very complicated, nor is it essential but it does make it much easier for us to manage permissions in our JavaScript-based apps. There will probably be some new features and changes to the API and we'll keep you updated!

  Tweet It

🕵 Search Results

🔎 Searching...