Using Netlify Functions to Create Mux Signing Tokens

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 JAMstack Conf 2019 in SF, 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: muxinc/examples/signed-playback-netlify.

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 (Mux API docs) 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 mux.com account (free account comes with $20 credit)
  • Go to settings/access-tokens 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:

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.

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 or 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.

JAMstack Conf SF

I (along with others from Mux) will be at JAMstack Conf 2019 in SF. If you're using Netlify Cloud Functions (or other cool JAMStack stuff) get in touch or come talk to me at the conference!