Mux

The Mux Developer Hub

Welcome to the Mux developer hub. You'll find comprehensive guides and documentation to help you start working with Mux as quickly as possible, as well as support if you get stuck. Let's jump right in!

Get Started

Security: Signed URLs

Mux Playback IDs have two types: public and signed. Public playback URLs can be watched anywhere, any time. This means that public playback URLs can be embedded on other sites. For example, a user could "View Source," find the m3u8 URL, and publish it somewhere else (or post it to Reddit, or email it to a friend).

If this is not what you want, use a signed URL. Signed URLs are signed server-side by your application and are only playable for a short time.

Here's how to do this.

1. Create an Asset with a "Signed" Playback Policy

A Mux Video asset can have multiple playback IDs, each with a playback policy that is public or signed. The playback policy can be specified when creating a new asset, or it can be added after the fact via the playback policy API.

// POST https://api.mux.com/video/assets
{
  "input": "https://storage.googleapis.com/muxdemofiles/mux-video-intro.mp4",
  "playback_policy": "signed"
}

See Create a playback ID to learn how to add a new playback ID to an existing asset.

2. Create a signing key

Signing keys can be managed (created, deleted, listed) via the Mux Video API. When creating a new signing key, the API will generate a 2048-bit RSA key-pair and return the private key and a generated key-id; the public key will be stored at Mux to validate signed tokens. Store the private-key in a secure manner.

Signing keys are created and deleted independently of assets. You probably only need one signing key active at a time, but you can create multiple to enable key rotation, creating a new key and deleting the old only after any existing signed URLs have expired.

See Create a URL signing key for full documentation.

// POST https://api.mux.com/video/v1/signing-keys
{
  "data": {
    "private_key": (base64-encoded PEM file with private key),
    "id": "(unique signing-key identifier)",
    "created_at": "(UNIX Epoch seconds)”
  }
}

3. Generate a JSON Web Token

All signed requests will have a JWT with at least the following standard claims:

Claim CodeDescriptionValue
subSubject of the JWTMux Video Playback ID
audAudience (intended application of the token)v (Video or Subtitles/Closed Captions)
t (Thumbnail)
g (Gif)
expExpiration timeUNIX Epoch seconds when the token expires. This should always exceed the current-time plus the duration of the video, else portions of the video may be unplayable.
kidKey IdentifierKey ID returned when signing key was created

The Thumbnail API accepts several options to control image selection and transformations.

For playback-id’s that use a public policy, the thumbnail options are supplied as query parameters on the request URL.

For playback-id’s that use a signed policy, the thumbnail options must be specified in the JWT claims when using signed URLs. This ensures that the thumbnail options are not altered, such as changing the timestamp or the dimensions of the thumbnail image. For example, if you uploaded a 4K video and wanted to restrict a thumbnail to a width of 600 pixels and a specific timestamp, then simply include the ‘width’ and ‘time’ keys in the JWT claims.

4. Signing the JWT

The steps can be summarized as:

  1. Load the private key used for signing
  2. Assemble the claims (sub, exp, kid, aud, etc) in a map
  3. Encode and sign the JWT using the claims map and private key and the RS256 algo.

There are dozens of software libraries for creating & reading JWTs. Whether you’re writing in Go, Elixir, Ruby, or a dozen other languages, don’t fret, there’s probably a JWT library that you can rely on.

// We've created some helper functions for Node to make your signing-life easier
const { JWT } = require('@mux/mux-node');

const playbackId = 'your-asset-playback-id';

// Set some base options we can use for a few different signing types
let baseOptions = {
  keyId: 'your-key-id',
  keySecret: 'your-key-secret',
};

const token = JWT.sign(playbackId, { ...baseOptions, type: 'video' });

// Now the signed playback url should look like this:
// `https://stream.mux.com/${playbackId}.m3u8?token=${token}`

// If you wanted to pass in params for something like a gif, use the
// `params` key in the options object
const gifToken = JWT.sign(playbackId, { 
  ...baseOptions, 
  type: 'gif',
  params: { time: 10 },
}
                          
// `https://image.mux.com/${playbackId}/animated.gif?token=${gifToken}`
require 'base64'
require 'jwt'

def sign_url(playback_id, audience, expires, signing_key_id, private_key, params = {})
    rsa_private = OpenSSL::PKey::RSA.new(Base64.decode64(private_key))
    payload = {sub: playback_id, exp: expires.to_i, kid: signing_key_id, aud: audience}
    payload.merge!(params)
    JWT.encode(payload, rsa_private, 'RS256')
end

sign_url('my_playback_id', 'v', Time.now + 3600, 'my_key_id', 'my_private_key')
# This example uses pyjwt:
# pip install pyjwt

import jwt
import base64
import time

playback_id = '' # enter your playback id here
signing_key_id = '' # enter your signing key id here
private_key_base64 = '' # enter your base64 encoded private key here

private_key = base64.b64decode(private_key_base64)

token = {
    'sub': playback_id,
    'exp': int(time.time()) + 3600, # 1 hour
    'aud': 'v'
}
headers = {
    'kid': signing_key_id
}

json_web_token = jwt.encode(
    token, private_key, algorithm="RS256", headers=headers)

# You need to decode the token, otherwise you'll get a `b'` at the start of the string.
print(json_web_token.decode('UTF-8'))
<?php 

  // Using https://github.com/firebase/php-jwt

  use \Firebase\JWT\JWT;

  $keyId = "your-key-id-from-keys-api";
  $keySecret = "your-secret-key-from-keys-api";

  $payload = array(
  "sub" => "your-private-playback-id",    // A private Playback ID
  "aud" => "v",                           // v = video, t = thumbnail, g = gif.
  "exp" => time() + 600,                  // Expiry time in epoch - in this case now + 10 mins
  "kid" => $keyId
  );

  $jwt = JWT::encode($payload, base64_decode($keySecret), 'RS256');

  print "$jwt\n";

?>
package main

import (
    "encoding/base64"
    "fmt"
    "log"
    "time"

    "github.com/dgrijalva/jwt-go"
)

func main() {

    keyId := "your-key-id-from-keys-api"
    key := "your-secret-key-from-keys-api"

    playbackId := "your-private-playback-id"

    decodedKey, err := base64.StdEncoding.DecodeString(key)
    if err != nil {
        log.Fatalf("Could not base64 decode private key: %v", err)
    }

    signKey, err := jwt.ParseRSAPrivateKeyFromPEM(decodedKey)
    if err != nil {
        log.Fatalf("Could not parse RSA private key: %v", err)
    }

    token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
        "sub": playbackId,
        "aud": "v",
        "exp": time.Now().Add(time.Minute * 15).Unix(),
        "kid": keyId,
    })

    tokenString, err := token.SignedString(signKey)
    if err != nil {
        log.Fatalf("Could not generate token: %v", err)
    }

    fmt.Println(tokenString)
}

5. Include the JWT in the media URL

Supply the JWT in the resource URL using the ‘token’ query parameter. The Mux Video service will enforce the inspect and validate the JWT to make sure the request is allowed.

Video URL example:

https://stream.mux.com/{playback-id}.m3u8?token={JWT}
https://stream.mux.com/{playback-id}.m3u8

Thumbnail options are supplied as query parameters when using a public policy. When using a signed policy, the thumbnail options must be specified as claims in the JWT following the same naming conventions as with query parameters.

Thumbnail URL example:

https://image.mux.com/{playback-id}/thumbnail.{format}?token={JWT}
https://image.mux.com/{playback-id}/thumbnail.{format}?{thumbnail options}

Security: what's next

In the future, Signed URLs will be expanded to support additional security features, like geographic restriction.

Get in touch if you have other security needs.

Note on query parameters after signing

When you're signing a URL, you're signing the params for that URL as well. After the params are signed for a playback ID, the resulting signed URL should only contain the token param. This is important because leaving the parameters in the URL would both:

  • expose more information about the underlying asset than you may want
  • result in an incorrect signature since the extraneous params would alter the URL.

Example:

Let's say we're taking the following public example and making a signed URL:

  • https://image.mux.com/{public_playback_id}/thumbnail.jpg?time=25

Generate a signed url with {time: 25} in the claims body. Using the helper example we wrote above, this would look like:

  • sign(signedPlaybackId, { ...requiredTokenOptions, params: { time: 25 } })

Correct Signed URL:

  • https://image.mux.com/{signed_playback_id}/thumbnail.jpg?token={token}

Bad Signed URL:

  • https://image.mux.com/{signed_playback_id}/thumbnail.jpg?time=25&token={token}

🚧

Be sure to include params in your claims body

While the JWT helper we expose in our Node SDK passes in additional params as an extra hash, when working with the JWT directly, these params should be embedded directly in your claims body.

Updated about a month ago

Security: Signed URLs


Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.