Using Netlify Functions to Create Signing Tokens
Tags: serverless
Published: 2019-11-07 20:38:31

Have you used cloud functions yet? They come in many flavors: Amazon Lambda, Cloudflare Workers, Zeit Serverless Functions, and the one we’re using here: Netlify Functions.

Cloud functions are an essential component of the JAMstack. I had not heard the name JAMstack until recently. For the uninitiated (like me) it stands for Javascript, APIs and Markup. You may have seen JAMstack technologies like Gatsby, Next.js and tools of this nature that focus on performance, new developer tooling, and leveraging CDNs to serve pre-compiled HTML pages. I will be at <a href="https://jamstackconf.com/sf/" target="_blank">JAMstack Conf 2019 in SF</a>, if you will be there too, then come find me and say hi!

All the code in this post is open source here on GitHub in our examples repo: <a href="https://github.com/muxinc/examples/tree/master/signed-playback-netlify" target="_blank">muxinc/examples/signed-playback-netlify</a>.

## The Main Benefits of Cloud Functions

1. Run code close to your clients (clients can be browsers, mobile apps, internet-of-things devices, self-driving cars, drones, anything that is talking to your server). Like a CDN, cloud functions are deployed to edge data centers to minimize latency between your clients and the server that runs their code.
2. Protect your origin servers from being flooded with traffic. Cloud functions are a good way to cache data and intercept requests and respond to your users before they reach your origin servers. This means less bandwidth and CPU that your origin servers have to process.

Cloud functions, like Netlify Functions, might be a good option for you if you are using Mux’s Signed URLs feature.

## A Little Background About Signed Urls

When you create a video asset via Mux’s `POST /video` API you can also create a Playback ID (<a href="https://docs.mux.com/docs/video" target="_blank">Mux API docs</a>) and specify the `playback_policy` as either `"public"` or `"signed"`.

A “public” playback policy can be played back on any site, in any player and does not have an expiration. A “signed” playback policy requires that when the playback URL is requested from a player, it has to be accompanied by a “token” param that is generated and signed on your server.

This is how it looks:

public playback URL:
`https://stream.mux.com/${playbackId}.m3u8`

signed playback URL:
`https://stream.mux.com/${playbackId}.m3u8?token=${token}`

The `token` param is what you need to create on your server in order for the playback URL to work.

## Create a Mux Asset

- Sign up for a <a href="https://mux.com" target="_blank">mux.com</a> account (free account comes with $20 credit)
- Go to <a href="https://mux.com/settings/access-tokens">settings/access-tokens</a> and click “Generate new token” to create a token you can use for API calls
- Copy your token id (we'll call this `MUX_TOKEN_ID`) and secret (`MUX_TOKEN_SECRET`). You will need these to make two api calls.
- Create a Mux video asset with a “signed” playback policy

```
curl https://api.mux.com/video/v1/assets \
  -X POST \
  -H "Content-Type: application/json" \
  -u ${MUX_TOKEN_ID}:${MUX_TOKEN_SECRET} \
  -d '{ "input": "https://storage.googleapis.com/muxdemofiles/mux-video-intro.mp4", "playback_policy": "signed" }' 
```


- Copy the `playback_id` from the response, you will need this later.

## Create a URL Signing Key


````
curl https://api.mux.com/video/v1/signing-keys \
  -X POST \
  -H "Content-Type: application/json" \
  -u ${MUX_TOKEN_ID}:${MUX_TOKEN_SECRET}
```


- Copy the `id` (`MUX_TOKEN_ID`) and the `private_key` (`MUX_PRIVATE_KEY`) from the response, you will need these later. These are the keys you will need to create signed urls for playback.

## Setup a Netlify project

- Create a new directory for your project `mkdir netlify-mux-signing && cd netlify-mux-signing`
- Install the Netlify CLI and run `netlify init` to create a new project. You can choose to connect Netlify to a GitHub repository.
- If you’re starting from scratch, run `yarn init` to create an empty `package.json` and `git init` to make this a git repository.
- Now you have a barebones project that is connected to Netlify, but nothing is in it yet (you can see there is a hidden and gitignored directory called `.netlify` which Netlify uses to handle deploys and Netlify commands
- Run `yarn add netlify-lambda` to install the netlify-lambda package into your project (it’s recommended to install this locally instead of globally).
- Run  `yarn add @mux/mux-node` to add the Mux node SDK to your project

### Step 1: Create a module to generate a signing token

Create a `src/` folder in your project and let’s create a small module called `mux_signatures.js`. It will export one function called signPlaybackId which takes a playback id and returns a token that is generated with `Mux.JWT.sign`:

```js
// ./src/mux_signatures

import Mux from '@mux/mux-node';

export const signPlaybackId = function (playbackId) {
  return Mux.JWT.sign(playbackId, {
    keyId: process.env.MUX_SIGNING_KEY,
    keySecret: process.env.MUX_PRIVATE_KEY
  })
}
```

Our lambda function is going to use this module in Step 2.

### Step 2: Create a `sign_playback_id` cloud function

Create a Netlify Function entry point. This is the single function that will handle one request. The idomatic pattern for creating cloud functions is to do one file and one javascript function per route. We will create a directory called `functions/` and add a file called `sign_playback_id.js`.


```js
// ./functions/sign_playback_ids.js

const keySecret = process.env.MUX_PRIVATE_KEY
import { signPlaybackId } from './src/mux_signatures';

exports.handler = async (event, context) => {
  try {
    const { queryStringParameters } = event;
    const { playbackId } = queryStringParameters;
    if (!playbackId) {
      return { statusCode: 400, body: JSON.stringify({errors: [{message: 'Missing playbackId in query string'}]}) };
    }
    const token = await signPlaybackId(playbackId);
    return  {
      statusCode: 302,
      headers: {
        'Access-Control-Allow-Origin': '*',
        location: `https://stream.mux.com/${playbackId}.m3u8?token=${token}`
      },
      body: '',
    }
  } catch (e) {
    console.error(e);
    return { statusCode: 500, body: JSON.stringify({ errors: [{message: 'Server Error'}] }) };
  }
}
```

### Step 3: Add netlify.toml

Add a `netlify.toml` file to the root directory and tell Netlify where your functions will live. This tells Netlify that before we deploy we are going to build our functions into the `./.netlify/functions` directory

```
    [build]
      functions = "./.netlify/functions"
```

### Step 4: Connect to git

In order to use Netlify Functions you will now need to commit your code and push it up to a git repository like GitHub. Do that next and in Netlify’s dashboard connect your git repository to the Netlify project that you created. After connecting you git repository then 

### Step 5: Set your environment variables

In your Netlify project dashboard, naviate to "Settings" > "Deploys" > “Environment” to set your environment variables. Enter the `MUX_SIGNING_KEY` and `MUX_PRIVATE_KEY` from the Create a URL Signing Key step above.

### Step 6: Test in development

1. Open one terminal and run `netlify dev` this will start a local Netlify dev server
2. Open another terminal window and run `netlify-lambda serve ./functions` this will build your functions/, get them ready to handle requests and watch the filesystem for changes.
3. In a third terminal window, curl your endpoint to test out the function (replace `<netlify-port>` and `<playback-id>` with your values.

```
curl -I 'http://localhost:<netlify-port>/.netlify/functions/sign_playback_id?playbackId=<playback-id>'
```

You should see a 302 (redirect) response with a `location` header for the signed url.

When you make any changes to your source files, `netlify-lambda serve` will pick up on the changes and recompile the functions into `./.netlify/functions`.


## Deploy

When you’re ready to deploy, you can deploy from the command line with `netlify-lambda build ./functions && netlify deploy --prod`. This will build the functions and then push up the changes to Netlify.

Try making a POST request to your cloud function on Netlify:

```
curl -I 'https://<your-netlify-app>.netlify.com/.netlify/functions/sign_playback_id?playbackId=<playback-id>'
```

Just like in dev, you should get back a 302 response with a `location` header that points to the signed playback URL.

    https://stream.mux.com/${playbackId}.m3u8?token=${token}

This is what your response should look like:

```
HTTP/2 302
access-control-allow-origin: *
cache-control: no-cache
location: https://stream.mux.com/jqi1UtiO3gccQ019UcYjGJTLO9Ee00TLMY.m3u8?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFsVFZncktBVTYzVldIdVplcDEwMVhZUk5mbHozeDIxRiJ9.eyJleHAiOjE1NzE3NjE0NzMsImF1ZCI6InYiLCJzdWIiOiJqcWkxVXRpTzNnY2NRMDE5VWNZakdKVExPOUVlMDBUTE1ZIn0.i7oANZ6inmwmGVQjon4WEv_gKcqQ2v8GuQA8xuCBdT0Reegkm6WyTdU-VloZvAt7duaRR3-T8dt147vUQjM1n70CLi0996pwMejYWIbRHUMqrDBtsENHG8T9jtz-EJcBGONSzgs7fBQIVQx8xJvPuX4YqpylDK_lNX0-RDqfhz5THAfuyxzePJod709msD8kbHAqnIke5lHzbQNHuO2ecNFVCb2ZozW7XkIEctyLxrDAK1ITtQV8iHek3whwO9S05kM-5bQzomJEliN3mXBqCwMBmyIp8l88YKl59tVXDdU-l-cZvZjt1GYKv0J7shO-oBYcr00NmVKkP7bie_w50w
date: Tue, 15 Oct 2019 16:24:33 GMT
age: 0
server: Netlify
x-nf-request-id: 80484951-e7ff-46f3-b78e-1349b8514bec-1426623
```

Now, in your player, you can use your netlify function as the URL src. Here's an example for a web player (note that in order to get HLS in a `<video>` tag to work outside of Safari you will need to use another library like [Video.js](https://videojs.com" target="_blank" or [HLS.js](https://github.com/video-dev/hls.js/):

```
<video src="https://<your-netlify-project>.netlify.com/.netlify/functions/sign_playback_id?playbackId=<playback-id>"></video>
```

And here's an example on iOS with AVPlayer in Swift:

```
let url = URL(string: "https://<your-netlify-project>.netlify.com/.netlify/functions/sign_playback_id?playbackId=<playback-id>")

player = AVPlayer(url: url!)
player!.play()
```

The player will load the netlify URL, get the 302 redirect to the signed Mux URL and load the HLS manifest from stream.mux.com.

## Restrict who can access your function

Now that your cloud function is working, you can add some security around it to make sure you only allow authorized users to generate signed urls.

For web players you will want to change this line in the `sign_playback_id.js` function:

```
'Access-Control-Allow-Origin': '*',
```

You can use the Access-Control-Allow-Origin header to control the CORS rules for the resource.