Better Deno Security: Ask for Permission at Runtime
Tags: deno, javascript, typescript
Published: 2020-05-28 13:46:00

Deno, the new kid on the block in server-side TypeScript and JavaScript, is secure by default. You kind of can’t miss it. They’ve been hammering that point all over their documentation and conference talks just to make sure you know. It’s on [their homepage](https://deno.land/) too, repeated in the first **three** sentences.

I'm glad they chose secure by default. But I'm not crazy about the samples they use to demonstrate security. The samples promote the idea that you should **specify in advance what an app’s permissions are going to be**. This reminds me of the old Android model, where you had to grant all permissions to an app when installing it. Eventually Android fixed it to be like the better model that iOS and browsers use: **let the program request the necessary permissions at runtime** so the user can respond to the request in context.

Let's look at sample code from the [Deno v1 announcement](https://deno.land/v1):

```ts
import { serve } from "https://deno.land/std@0.50.0/http/server.ts";

for await (const req of serve({ port: 8000 })) {
  req.respond({ body: "Hello World\n" });
}
```

I’ve copied this sample to a GitHub repo so you can run it directly:

```
$ deno run https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/serve-original.ts
```

and you’ll see that it fails, as it should, because it doesn’t have network permission. The v1 page explains this: "The above example will fail unless the `--allow-net` command-line flag is provided."

![Deno network permission error](https://dev-to-uploads.s3.amazonaws.com/i/8i0yept66z5lk1aiahw2.png)

Fix by running with `--allow-net`.

```
$ deno run --allow-net https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/serve-original.ts
```

(You won’t see any output because it’s silently waiting on a port.)

Sounds nice and secure. But when a user downloads a script and runs it, or runs it directly off the Internet as in the examples above, they don’t have advance knowledge of why the program needs network permission—or worse, file permission. So they’ll blindly grant it **all** the permissions it says it needs before they even run it.

This is similar to what [used to be required when installing an Android app](https://developer.android.com/guide/topics/permissions/overview#install-time_requests_android_511_and_below). When I had an Android phone I found it vexing. Sometimes I wanted an app but didn’t want to grant it access to, for example my contacts. The slight improvement with Deno is that at least with Deno, permissions aren’t all-or-nothing, chosen by the developer.

## A better option: ask for permission at runtime

Thankfully, Deno already provides a better security model than this. It’s just not heavily promoted. The program can ask for permission at the point of need, rather than requiring the user to grant it up front on the command line.

```ts
// serve-request.ts
import { serve } from "https://deno.land/std@0.50.0/http/server.ts";

const netPermission = await Deno.permissions.request({ name: "net" });
if (netPermission.state === "granted") {
  for await (const req of serve({ port: 8000 })) {
    req.respond({ body: "Hello World\n" });
  }
} else {
  console.log("Can’t serve pages without net permission.");
}
```

At the time of this writing, `Deno.permissions` is not yet part of the stable API so you need to run this with `--unstable`:

```
deno run --unstable https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/serve-request.ts
```

Here’s how this looks when granting permission (I typed `g` and hit `Enter`):

![Ask for permissions at runtime](https://dev-to-uploads.s3.amazonaws.com/i/3ie228vzxgu95x1qluk7.png)

And here’s how it looks when denying:

![Denying permissions at runtime](https://dev-to-uploads.s3.amazonaws.com/i/w0fzlf7wm3vx3teuqpjx.png)

The "Deno requests" line is part of Deno itself, not controlled by the app. As you can see from the code above, the “Can’t serve pages without net permission” line is the app’s custom response. An app can respond any way it likes to the user’s choice not to grant permission.

Don’t worry about Deno asking for permissions that are already granted. If the user runs the app with `--allow-net`, the `Deno.permissions.request` call won’t redundantly ask for permission. It will just return `{ "state": "granted" }` immediately.

The same is true if an app requests the same permission multiple times at runtime. If it’s already been granted or denied once during runtime, the response is remembered in all subsequent permission requests.

```ts
// request-twice.ts
const netPermission1 = await Deno.permissions.request({ name: "net" });
console.log(
  `The first permission request returned ${JSON.stringify(netPermission1)}`,
);
const netPermission2 = await Deno.permissions.request({ name: "net" });
console.log(
  `The second permission request returned ${JSON.stringify(netPermission2)}`,
);
```

Run:

```
$ deno run --unstable https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/request-twice.ts
```

As you can see, it only asks for permissions once:

![Request permission twice](https://dev-to-uploads.s3.amazonaws.com/i/76w4dvddci2zo9d5p6yh.png)

## Give context when asking for permission

The [iOS Human Interface Guidelines on permissions](https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/requesting-permission/#:~:text=Explain%20why%20your%20app%20needs%20the%20information.) state: 

> Explain why your app needs the information. Provide custom text (known as a purpose string or usage description string) for display in the system's permission request alert, and include an example. Keep the text short and specific, use sentence case, and be polite so people don't feel pressured. There’s no need to include your app name—the system already identifies your app.

This is good advice for Deno apps as well. If you give the user context about why you need permission, they’re more likely to grant it:

```ts
// serve-context.ts
import { serve } from "https://deno.land/std@0.50.0/http/server.ts";

let netPermission = await Deno.permissions.query({ name: "net" });
if (netPermission.state === "prompt") {
  console.log("Net permission needed to serve web pages.");
  netPermission = await Deno.permissions.request({ name: "net" });
}
if (netPermission.state === "granted") {
  for await (const req of serve({ port: 8000 })) {
    req.respond({ body: "Hello World\n" });
  }
} else {
  console.log("Can’t serve pages without net permission.");
}
```

This version prints the reason for the request right before making the request. The gives the user enough context to understand why Deno is requesting permission. 

But what’s this call to `Deno.permissions.query`? Now that we’re displaying some context before the permission prompt is shown, it’s necessary to first check if the app already has permission. Otherwise you’ll display the permission context for no reason.

`Deno.permissions.query` can return three possible states:
1. `{ "state": "granted" }` means you already have permission.
2. `{ "state": "denied" }` means you have already been denied permission. There’s no point in requesting it because it will return "denied" immediately without showing a prompt.
3. `{ "state": "prompt" }` means if you call `request`, a prompt will be shown to the user.

Let’s run it:

```
deno run --unstable https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/serve-context.ts
```

And you’ll see the prompt:

![Give context before requesting permission](https://dev-to-uploads.s3.amazonaws.com/i/ycz7ua85gcau7fqq6zay.png)

If you run with `--allow-net`, you can see that it doesn’t display the context because the call to `Deno.permissions.query` indicated that the call to `Deno.permissions.request` would return success.

In my opinion this is the best way to deal with permissions in your code.

## Wish list: store permissions persistently for installed scripts

Deno has a `deno install` command that lets you add a Deno script to your machine:

```
$ deno install --unstable https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/serve-context.ts
```

This downloads, compiles, and caches the script and its dependencies. It also creates an executable script. On a Mac it’s stored in `~/.deno/bin/serve-context` (you can add `~/.deno/bin` to your PATH) and looks like this:

```sh
#!/bin/sh
# generated by deno install
deno "run" "--unstable" "https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/serve-context.ts" "$@"
```

Notice that parameters you pass to `deno install`, such as `--unstable`, get passed on to `deno run`. You could `deno install --allow-net https://my/script` and it would store the `--allow-net` permission in the executable.

Again, it’s not ideal to require your users to decide up front what all the permissions are. But Deno doesn’t store the permissions persistently. After installing `serve-context` without `--allow-net`, every time I run it, it asks for net permission.

**I’d love to see an improvement to Deno that allows it to locally cache the user’s answers to permission questions per app.** This is what browsers do when a domain requests permission, such as camera or geolocation. Of course you’d also need a way to revoke or grant permissions after the fact.

## Wish list: let the script pass the reason into the `request` call

In the `serve-context` example we had to:

1. Call `Deno.permissions.query` to see if a permission has been granted.
2. If not:
    1. Display a reason that the script needs the permission
    2. Request it with `Deno.permissions.request`.

It’d be a lot simpler if you could do all this in a single call to `Deno.permissions.request`. **I think Deno should let the script pass in a short `reason` string to the permission request. If the script doesn’t already have permission, the reason would be displayed before the user is asked to grant permission**. If the script already has permission, the reason won’t be displayed.

If Deno supported this, the steps would be shortened to just:

1. Request permission with `Deno.permissions.request`.

Here is what the code would look like (not runnable because `reason` is not currently a valid key to pass in to `request`):

```ts
// serve-reason.ts
import { serve } from "https://deno.land/std@0.50.0/http/server.ts";

const netPermission = await Deno.permissions.request(
  { name: "net", reason: "Net permission needed to serve web pages." },
);
if (netPermission.state === "granted") {
  for await (const req of serve({ port: 8000 })) {
    req.respond({ body: "Hello World\n" });
  }
} else {
  console.log("Can’t serve pages without net permission.");
}
```

## References

* [Deno permissions documentation](https://deno.land/typedoc/classes/deno.permissions.html). The list of permission types is in the `PermissionDescriptor` type, but as of this writing, Deno doesn't seem to have docs for this type.
* [Current list of `PermissionDescriptor` values, currently in unstable.ts](https://github.com/denoland/deno/blob/f6e58b076ca430c29f164b65678bbf6bc64a39b7/cli/js/lib.deno.unstable.d.ts#L1122-L1184).
* You can [follow me on Twitter](https://twitter.com/BMorearty).