Signed URLs

By default, videos on Cloudflare Stream can be viewed by anyone anytime until you delete the video. Users can view the source of the embed code in their browser and get a URL to the video that could be shared with others. To prevent this, use signed URLs.

Signed URLs are controlled by you and and can be set to expire after a set time period. This allows you to control the time window you allow your users to watch videos. Other viewing constraints can be applied.

To implement signed URLs

  1. Create a key
  2. Make a video require signed URLS
  3. Sign tokens to use in embed code

These steps are detailed below.

You can revoke a key anytime for any reason.

Creating a signing key

Upon creation you will get a RSA private key in PEM and JWK formats. Keys are created, used and deleted independently of videos. Every key can sign any of your videos.

// curl -X POST -H "X-Auth-Email: ${EMAIL}" -H "X-Auth-Key: ${API-KEY}"  "https://api.cloudflare.com/client/v4/accounts/{account_id}/media/keys"

{
  "result": {
    "id": "{KEY-ID}",
    "pem": "{PRIVATE-KEY-IN-PEM-FORMAT}",
    "jwk": "{PRIVATE-KEY-IN-JWK-FORMAT}",
    "created": "{TIMESTAMP}"
  },
  "success": true,
  "errors": [],
  "messages": []
}

Making a video require signed URLs

Since video ids are effectively public within signed URLs, you will need to turn on requireSignedURLs on for your videos. This option will prevent any public links, such as watch.cloudflarestream.com/{VIDEO-ID}, from working.

Restricting viewing can be done by updating the video’s metadata.

// curl -X POST -H "X-Auth-Email: ${EMAIL}" -H "X-Auth-Key: ${API-KEY}"  "https://api.cloudflare.com/client/v4/accounts/{account_id}/media/{VIDEO-ID}" -H "Content-Type: application/json" -d '{"uid": "{VIDEO-ID}", "requireSignedURLs": true }'

{
  "result": {
    "uid": "<{VIDEO-ID}>",
    ...
    "requireSignedURLS": true
  },
  "success": true,
  "errors": [],
  "messages": []
}

Signing unique tokens

After creating a key, you can use it to sign unique signed tokens. These tokens can be used in place of video ids in the stream embed code.

You can sign to assert these optional constraints on the token:

  • exp - expiration; a unix epoch timestamp after which the token will not be accepted.
  • nbf - notBefore; a unix epoch timestamp before which the token will not be accepted.

Get started with a signing utility

Using this signing utility in production is not recommended.

We offer a utility at https://util.cloudflarestream.com/sign to generate tokens when getting familiar with signed URLs.

curl -X POST "https://util.cloudflarestream.com/sign/{VIDEO-ID}" -d '{"id": "{KEY-ID}", "pem": "{PRIVATE-KEY-IN-PEM-FORMAT}","nbf":1537453165,"exp":1537460365}'

This endpoint accepts JSON bodies with the output from Creating a signing key or any object with pem and kid attributes. To add a constraint, include it as a property of the body.

Signing tokens in production

var jwt = require('jsonwebtoken');

var token = jwt.sign(

  { kid: "{KEY-ID}",
    sub: "{VIDEO-ID}",
  },
  Buffer.from("{PRIVATE-KEY-IN-PEM-FORMAT}",'base64'),
  {
    expiresIn: '1h', // or preferred expiry time. Note that this should be longer than the duration of the video
    algorithm: 'RS256',
  }
);


// you can now use the token instead of the video id when viewing videos
console.log(token)

Other offline signing examples are included below

Revoking keys

You can create up to 1,000 keys and rotate them at your convenience. Once revoked all tokens created with that key will be invalidated.

// curl -X DELETE -H "X-Auth-Email: ${EMAIL}" -H "X-Auth-Key: ${API-KEY}"  "https://api.cloudflare.com/client/v4/accounts/{account_id}/media/keys/{KEY-ID}"

{
  "result": "Revoked",
  "success": true,
  "errors": [],
  "messages": []
}

Other offline signing examples

Sign in go using go-jose

Use: go run sign.go

package main

import (
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"fmt"
	"os"
	"time"

	jose "gopkg.in/square/go-jose.v2"
	jwt "gopkg.in/square/go-jose.v2/jwt"
)

type claims struct {
	KeyID     string          `json:"kid,omitempty"`
	VideoID   string          `json:"sub,omitempty"`
	Expiry    jwt.NumericDate `json:"exp,omitempty"`
	NotBefore jwt.NumericDate `json:"nbf,omitempty"`
}

const videoID = "{VIDEO-ID}"
const keyID = "{KEY-ID}"
const privateKey = "{PRIVATE-KEY-IN-PEM-FORMAT}"
const expiresIn = time.Hour

func main() {
	// Decode privateKey
	keyBytes, err := base64.StdEncoding.DecodeString(privateKey)
	if err != nil {
		fmt.Printf("failed to generate key: %s\n", err)
		os.Exit(1)
	}
	block, _ := pem.Decode(keyBytes)
	if err != nil {
		fmt.Printf("failed to decode pem: %s\n", err)
		os.Exit(1)
	}
	rsaPrivateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		fmt.Printf("failed to parse key: %s\n", err)
		os.Exit(1)
	}

	// Prepare to sign
	var options jose.SignerOptions
	options.WithType("JWT").WithHeader("kid", keyID)
	signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: rsaPrivateKey},
		&options)
	if err != nil {
		fmt.Printf("failed to initialize signer: %s\n", err)
		os.Exit(1)
	}

	// Sign a JWT
	builder := jwt.Signed(signer)
	builder = builder.Claims(claims{
		KeyID:   keyID,
		VideoID: videoID,
		Expiry:  jwt.NewNumericDate(time.Now().Add(expiresIn)),
	})
	token, err := builder.CompactSerialize()
	if err != nil {
		fmt.Printf("failed to get token: %s\n", err)
		os.Exit(1)
	}

	fmt.Println(token)
}