support Contact Support | system status System Status

Creating a JSON Web Token (JWT)

In this topic, you will learn how to create a JSON Web Token (JWT) which can be used when communicating with Brightcove Playback API.

Introduction

To add an extra level of protection when accessing your video library, or to apply user-level restrictions for your content, you can pass a JSON Web Token (JWT) with your call to the Brightcove Playback API. To create the token, follow these steps:

  1. Generate public-private key pair
  2. Register public key with Brightcove
  3. Create a JSON Web Token
  4. Test playback

Generate public-private key pair

The publisher will generate a public-private key pair and provide the public key to Brightcove. The private key is used by the publisher to sign tokens, and is not shared with Brightcove.

There are many ways to generate the public-private key pair. Here are some examples:

Example bash script:

Example script to generate the key pair:

#!/bin/bash
set -euo pipefail

NAME=${1:-}
test -z "${NAME:-}" && NAME="brightcove-playback-auth-key-$(date +%s)"
mkdir "$NAME"

PRIVATE_PEM="./$NAME/private.pem"
PUBLIC_PEM="./$NAME/public.pem"
PUBLIC_TXT="./$NAME/public_key.txt"

ssh-keygen -t rsa -b 2048 -m PEM -f "$PRIVATE_PEM" -q -N ""
openssl rsa -in "$PRIVATE_PEM" -pubout -outform PEM -out "$PUBLIC_PEM" 2>/dev/null
openssl rsa -in "$PRIVATE_PEM" -pubout -outform DER | base64 > "$PUBLIC_TXT"

rm "$PRIVATE_PEM".pub

echo "Public key to saved in $PUBLIC_TXT"

Run the script:

$ bash keygen.sh

Example using Go

Example using the Go programming language to generate the key pair:

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"strconv"
	"time"
)

func main() {
	var out string

	flag.StringVar(&out, "output-dir", "", "Output directory to write files into")
	flag.Parse()

	if out == "" {
		out = "rsa-key_" + strconv.FormatInt(time.Now().Unix(), 10)
	}

	if err := os.MkdirAll(out, os.ModePerm); err != nil {
		panic(err.Error())
	}

	priv, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		panic(err.Error())
	}

	privBytes := x509.MarshalPKCS1PrivateKey(priv)

	pubBytes, err := x509.MarshalPKIXPublicKey(priv.Public())
	if err != nil {
		panic(err.Error())
	}

	privOut, err := os.OpenFile(path.Join(out, "private.pem"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		panic(err.Error())
	}

	if err := pem.Encode(privOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes}); err != nil {
		panic(err.Error())
	}

	pubOut, err := os.OpenFile(path.Join(out, "public.pem"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		panic(err.Error())
	}

	if err := pem.Encode(pubOut, &pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes}); err != nil {
		panic(err.Error())
	}

	var pubEnc = base64.StdEncoding.EncodeToString(pubBytes)

	var pubEncOut = path.Join(out, "public_key.txt")
	if err := ioutil.WriteFile(pubEncOut, []byte(pubEnc+"\n"), 0600); err != nil {
		panic(err.Error())
	}

	fmt.Println("Public key saved in " + pubEncOut)
}

Example using node.js

Example using node.js to generate the key pair:

var crypto = require("crypto");
var fs = require("fs");

var now = Math.floor(new Date() / 1000);
var dir = "rsa-key_" + now;
fs.mkdirSync(dir);

crypto.generateKeyPair(
  "rsa",
  {modulusLength: 2048},
  (err, publicKey, privateKey) => {
    fs.writeFile(
      dir + "/public.pem",
      publicKey.export({ type: "spki", format: "pem" }),
      err => {}
    );
    fs.writeFile(
      dir + "/public_key.txt",
      publicKey.export({ type: "spki", format: "der" }).toString("base64") +
        "\n",
      err => {}
    );
    fs.writeFile(
      dir + "/private.pem",
      privateKey.export({ type: "pkcs1", format: "pem" }),
      err => {}
    );
  }
);

console.log("Public key saved in " + dir + "/public_key.txt");

Register public key

You will use the Key API to register your public key with Brightcove.

Key API

The Key API is used to manage your public keys with Brightcove.

Base URL

The base URL for the API is:

https://playback-auth.api.brightcove.com

Account path

In all cases, requests will be made for a specific Video Cloud Account. So, you will always add the term accounts followed by your account id to the base URL:

https://playback-auth.api.brightcove.com/v1/accounts/{accountID}

Authorization

An access token for requests is required and must be present in the Authorization header::

Authorization: Bearer {access_token}

The access token is a temporary OAuth2 access token that must be obtained from the Brightcove OAuth service. For details on how to obtain client credentials and use them to retrieve access tokens, see the Brightcove OAuth Overview.

Permissions

Requests to the Key API must be made from client credentials with the following permissions:

  • video-cloud/playback-auth/key/read
  • video-cloud/playback-auth/key/write

Manage keys

The Key API supports the following requests:

Register a new key:

Put the value of your public key in the API request body. You can find the key in the public_key.txt file.

Request
POST /v1/accounts/{accountID}/keys
    Content-Type: application/json
    Body: {"value": "MFkwEwYHKoZIzj0CAQYIKoZIzj...MyeQviqploA=="}
Using Curl
curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {access_token}" \
  -d '{"value": "{your_public_key_value}"}' \
https://playback-auth.api.brightcove.com/v1/accounts/{accountID}/keys
Response
{
  "id": "{your_public_key_id}",
  "type": "public",
  "algorithm": "rsa",
  "value": "{your_public_key_value}",
  "createdAt": "2020-01-03T20:30:36.488Z"
}

List keys:

Get a list of public keys in your account.

GET /v1/accounts/{accountID}/keys

Get one key:

Get the details for a public key in your account.

GET /v1/accounts/{accountID}/keys/{key_Id}

Delete one key:

Delete a public key in your account.

DELETE /v1/accounts/{accountID}/keys/{key_Id}

Create a JSON Web Token

Publishers create a JSON Web Token (JWT). The token is signed with the RSA algorithm using the SHA-256 hash algorithm (identified in the JWT spec as "RS256") No other JWT algorithms will be supported.

A subset of the standard JSON Web Token claims will be used, along with some private claims defined by Brightcove. You will create a JSON Web Token signed with your private key.

Claims for Static URL Delivery

For a list of claims that can be used, see the Static URL Delivery document.

Generate a token

Libraries are commonly available to generate JWT tokens. For details, see the JSON Web Tokens site.

Example bash script:

Example script to generate the JWT token:

#! /usr/bin/env bash
# Static header fields.
HEADER='{
	"type": "JWT",
	"alg": "RS256"
}'

payload='{
	"pkid": "{your_public_key_id}",
	"accid": "{your_account_id}"
}'

# Use jq to set the dynamic `iat` and `exp`
# fields on the payload using the current time.
# `iat` is set to now, and `exp` is now + 1 second.
PAYLOAD=$(
	echo "${payload}" | jq --arg time_str "$(date +%s)" \
	'
	($time_str | tonumber) as $time_num
	| .iat=$time_num
	| .exp=($time_num + 60 * 60)
	'
)

function b64enc() { openssl enc -base64 -A | tr '+/' '-_' | tr -d '='; }

function rs_sign() { openssl dgst -binary -sha256 -sign playback-auth-keys/private.pem ; }

JWT_HDR_B64="$(echo -n "$HEADER" | b64enc)"
JWT_PAY_B64="$(echo -n "$PAYLOAD" | b64enc)"
UNSIGNED_JWT="$JWT_HDR_B64.$JWT_PAY_B64"
SIGNATURE=$(echo -n "$UNSIGNED_JWT" | rs_sign | b64enc)

echo "$UNSIGNED_JWT.$SIGNATURE"

Run the script:

$ bash jwtgen.sh

Example using Go

Here is an example of a reference Go implementation (as a cli tool) for generating tokens without the use of any 3rd party library:

package main

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"strings"
	"time"
)

// Header is the base64UrlEncoded string of a JWT header for the RS256 algorithm
const RSAHeader = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"

// Header is the base64UrlEncoded string of a JWT header for the EC256 algorithm
const ECHeader = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9"

// Claims represents constraints that should be applied to the use of the token
type Claims struct {
	Iat   float64 `json:"iat,omitempty"`   // Issued At
	Exp   float64 `json:"exp,omitempty"`   // Expires At
	Accid string  `json:"accid,omitempty"` // Account ID
	Conid string  `json:"conid,omitempty"` // Content ID
	Maxu  float64 `json:"maxu,omitempty"`  // Max Uses
	Maxip float64 `json:"maxip,omitempty"` // Max IPs
	Ua    string  `json:"ua,omitempty"`    // User Agent
}

func main() {
	var key, algorithm string

	c := Claims{Iat: float64(time.Now().Unix())}

	flag.StringVar(&key, "key", "", "Path to private.pem key file")
	flag.StringVar(&c.Accid, "account-id", "", "Account ID")
	flag.StringVar(&c.Conid, "content-id", "", "Content ID (eg, video_id or live_job_id)")
	flag.Float64Var(&c.Exp, "expires-at", float64(time.Now().AddDate(0, 0, 1).Unix()), "Epoch timestamp (in seconds) for when the token should stop working")
	flag.Float64Var(&c.Maxu, "max-uses", 0, "Maximum number of times the token is valid for")
	flag.Float64Var(&c.Maxip, "max-ips", 0, "Maximum number of unique IP addresses the token is valid for")
	flag.StringVar(&c.Ua, "user-agent", "", "User Agent that the token is valid for")
	flag.StringVar(&algorithm, "algo", "", "Key algorithm to use for signing. Valid: ec256, rsa256")
	flag.Parse()

	if key == "" {
		fmt.Printf("missing required flag: -key\n\n")
		flag.Usage()
		os.Exit(1)
	}

	if algorithm == "" {
		fmt.Printf("missing required flag: -algo\n\n")
		flag.Usage()
		os.Exit(2)
	}

	if algorithm != "rsa256" && algorithm != "ec256" {
		fmt.Printf("missing valid value for -algo flag. Valid: rsa256, ec256\n\n")
		flag.Usage()
		os.Exit(3)
	}

	if c.Accid == "" {
		fmt.Printf("missing required flag: -account-id\n\n")
		flag.Usage()
		os.Exit(4)
	}

	bs, err := json.Marshal(c)
	if err != nil {
		fmt.Println("failed to marshal token to json", err)
		os.Exit(5)
	}

	kbs, err := ioutil.ReadFile(key)
	if err != nil {
		fmt.Println("failed to read private key", err)
		os.Exit(6)
	}

	if algorithm == "rsa256" {
		processRSA256(kbs, bs)
	} else {
		processEC256(kbs, bs)
	}
}

func processRSA256(kbs, bs []byte) {
	block, _ := pem.Decode(kbs)
	if block == nil {
		fmt.Println("failed to decode PEM block containing private key")
		os.Exit(7)
	}

	if block.Type != "RSA PRIVATE KEY" {
		fmt.Println("failed to decode PEM block containing private key")
		os.Exit(8)
	}

	pKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		fmt.Println("failed to parse rsa private key", err)
		os.Exit(9)
	}

	message := RSAHeader + "." + base64.RawURLEncoding.EncodeToString(bs)

	hash := crypto.SHA256
	hasher := hash.New()
	_, _ = hasher.Write([]byte(message))
	hashed := hasher.Sum(nil)

	r, err := rsa.SignPKCS1v15(rand.Reader, pKey, hash, hashed)
	if err != nil {
		fmt.Println("failed to sign token", err)
		os.Exit(10)
	}

	sig := strings.TrimRight(base64.RawURLEncoding.EncodeToString(r), "=")

	fmt.Println(message + "." + sig)
}

func processEC256(kbs, bs []byte) {
	block, _ := pem.Decode(kbs)
	if block == nil {
		fmt.Println("failed to decode PEM block containing private key")
		os.Exit(7)
	}

	if block.Type != "EC PRIVATE KEY" {
		fmt.Println("failed to decode PEM block containing private key")
		os.Exit(8)
	}

	pkey, err := x509.ParseECPrivateKey(block.Bytes)
	if err != nil {
		fmt.Println("failed to parse ec private key", err)
		os.Exit(9)
	}

	message := ECHeader + "." + base64.RawURLEncoding.EncodeToString(bs)
	hash := sha256.Sum256([]byte(message))

	r, s, err := ecdsa.Sign(rand.Reader, pkey, hash[:])
	if err != nil {
		fmt.Println("failed to sign token", err)
		os.Exit(10)
	}

	curveBits := pkey.Curve.Params().BitSize

	keyBytes := curveBits / 8
	if curveBits%8 > 0 {
		keyBytes++
	}

	rBytes := r.Bytes()
	rBytesPadded := make([]byte, keyBytes)
	copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)

	sBytes := s.Bytes()
	sBytesPadded := make([]byte, keyBytes)
	copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)

	out := append(rBytesPadded, sBytesPadded...)

	sig := base64.RawURLEncoding.EncodeToString(out)
	fmt.Println(message + "." + sig)
}

Results

Here is an example of a decoded token using https://JWT.io specifying the full set of claims:

HEADER:

{
  "alg": "RS256",
  "type": "JWT"
}

PAYLOAD:

{
  "accid": "1100863500123",
  "conid": "51141412620123",
  "exp": 1554200832,
  "iat": 1554199032,
  "maxip": 10,
  "maxu": 10,
  "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36"
}

Test playback

Although not required, you may want to test video playback before configuring a player.

Request playback:

curl -X GET \
 -H 'Authorization: Bearer {JWT}' \
 https://edge-auth.api.brightcove.com/playback/v1/accounts/{your_account_id}/videos/{your_video_id}

Page last updated on 02 Apr 2020