Creating a JSON Web Token (JWT)

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

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.

If you are new to JWT's, review the following:

Workflow

To create a JSON Web Token (JWT) and register with Brightcove, 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

You (the publisher) will generate a public-private key pair and provide the public key to Brightcove. You will use the private key to sign tokens. The private key 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 own the private key, and you will use it to generate signed tokens. You will share the public key with Brightcove to verify your tokens. The key API allows you to register your public key with Brightcove.

For API details, see the Using Authentication APIs document.

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

The following claims can be used with Brightcove's Static URL Delivery.

Claim Type Required Description
accid String The account id that owns the content being played
iat Integer Time this token was issued, in seconds since the Epoch
exp Integer Time this token will no longer be valid, in seconds since the Epoch. Must be no more than 30 days from iat
drules String[] List of delivery rule action IDs to apply. For details, see the Implementing Delivery Rules document.
If the config_id query parameter is also set it will be ignored, as this claim overrides it.
conid String If present, this token will only authorize a specific Video Cloud video ID. This can be either a DRM/HLSe stream or a non-DRM asset.

Must be a valid video ID. Note that reference ID is not supported.
pro String Specifies a protection type in the case where multiple are available for a single video.

Values:
  • "" (default for clear content)
  • "aes128"
  • "widevine"
  • "playready"
  • "fairplay"
vod Object Contains specific configuration options for Video-On-Demand.
vod.ssai String Your Server-Side Ad Insertion (SSAI) configuration id. This claim is required to retrieve either an HLS or a DASH VMAP.
aud String Yes* The audience (recipient) for which the JWT is intended.

* This claim is optional for now, but will be soon enforced. Once it is enforced, JWT tokens submitted to the Playback API with no aud claim will be rejected. If the aud claim is present, it must contain the value static.api.brightcove.com within the claims array of values.


Here is an example of the JSON Web Token (JWT) claims that you might use:

{
// account id: JWT is only valid for this accounts
"accid":"4590388311111",
// issued at: timestamp when the JWT was created
"iat":1575484132,
// expires: timestamp when JWT expires
"exp":1577989732,
// drules: list of delivery rule IDs to be applied
"drules": ["0758da1f-e913-4f30-a587-181db8b1e4eb"],
// content id: JWT is only valid for video ID
"conid":"5805807122222",
// protection: specify a protection type in the case where multiple are available for a single video
"pro":"aes128",
// VOD specific configuration options
"vod":{
// SSAI configuration to apply
"ssai":"efcc566-b44b-5a77-a0e2-d33333333333"
}
}

Claims for Playback Restrictions

The following claims can be used with Brightcove Playback Restrictions. As a part of Playback Restrictions, you can implement the following:

Feature Claim Type Required for feature DRM only Description
General accid String Yes The account id that owns the content being played
aud Array of Strings The audience (recipient) for which the JWT is intended.

This claim is optional. JWT tokens submitted to the Playback API with no aud claim will continue to work.

If the aud claim is present, it must contain the value playback.api.brightcove.com within the claims array of values.
exp Integer Yes Time this token will no longer be valid, in seconds since the Epoch. Must be no more than 30 days from iat
nbf Integer Time this token will begin being valid, in seconds since the Epoch.
If not specified, the token is available immediately.
iat Integer Yes Time this token was issued, in seconds since the Epoch
ip String Allows you to override the client ip used to evaluate geo-restrictions. The value must be a valid ipv4 (no short form) or ipv6.

For example, you can use this claim if you have a proxy in front of the Brightcove Playback API call. Setting the ip claim to the end user's IP address allows the authorization service to evaluate geo-restrictions for the end user's IP instead of your proxy's IP.
Playback Rights prid String A playback_rights_id used to override the id set in the catalog for this video

This field is not validated

tags Array of Strings if present, this token is only valid for videos that have the listed tags values. Only these videos are authorized for playback.
vids Array of Strings If present, this token will only authorize license fetching for a set of video IDs.

License Keys Protection ua String If present, this token will only be valid for requests from this User-Agent.

This field does not have to follow any particular format.
You must have License Keys Protection enabled.
conid String If present, this token will only authorize license fetching for a specific Video Cloud video id.

Must be a valid video id
You must have License Keys Protection enabled.
maxip Integer Yes If present, this token will only be able to be used by this number different IP addresses.

Required for session tracking; HLSe (AES-128) only
You must have License Keys Protection enabled.
maxu Integer Yes If present, this token will only be valid for this number of license requests.

  • For HLSe, players will make multiple requests when playing a video, typically one per rendition. The maxu must be set high enough to account for these additional requests.
Required for session tracking; HLSe (AES-128) only
You must have License Keys Protection enabled.
Concurrent streams uid String Yes Yes The user id of the end viewer. This field is used to correlate multiple sessions to enforce Stream Concurrency.

You can use an arbitrary id (max. 64 characters, limited to A-Z, a-z, 0-9, and =/,@_.+-). But, depending on your use case, Brightcove recommends either a user identifier to track sessions per user or an account identifier to track sessions per paying account.

Required for session concurrency
climit Integer Yes Yes When this field is included, it enables Stream Concurrency checking along with license renewal requests. This value indicates the number of concurrent watchers allowed.

Required for session concurrency
cbeh String Yes Set the value to BLOCK_NEW to enable concurrent stream limits to block any new request, even from the same user, when the maximum number of streams is reached.

Set the value to BLOCK_NEW_USER to block any new request only from a new user when the maximum number of streams is reached.

The default blocks the oldest stream when the maximum number of streams is reached.
sid String Yes Specifying the Session ID of the current stream allows you to control how a session is defined. By default, a session is defined as User-Agent (browser) + IP address + video id.

For example, you can loosen the definition of session to IP address + video ID

Device limits uid String Yes Yes The user id of the end viewer. This field is used to correlate multiple sessions to enforce Stream Concurrency.

You can use an arbitrary id (max. 64 characters, limited to A-Z, a-z, 0-9, and =/,@_.+-). But, depending on your use case, Brightcove recommends either a user identifier to track sessions per user or an account identifier to track sessions per paying account.

Required for device registration
dlimit Integer Yes Yes When this field is included, it controls how many devices can be associated with the specified user (uid). Value must be > 0.

Previously allowed devices will continue to operate if the dlimit value is dropped in later requests.

Example: if value is set to 3, the user can play on devices A, B, & C (all would be allowed). Trying to play on device D would be denied.

If value is changed to 1, the user can still play on all 3 devices A, B, & C, unless the devices are manually revoked by managing devices with the Playback Rights API.

Required for device registration
Delivery Rules drules String The delivery rule action ID to apply. For details, see the Implementing Delivery Rules document.

Claims by tier

Several security packages are available for Playback Restrictions. For details, see the Overview: Brightcove Playback Restrictions document.

Here are the claims available for each Playback Restriction package:

Feature Claims Security Tier 1 Security Tier 2 Security Tier 3
General accid Yes Yes Yes
aud Yes Yes Yes
iat Yes Yes Yes
exp Yes Yes Yes
nbf Yes Yes Yes
Playback Rights [1] prid Yes Yes Yes
tags Yes Yes Yes
vids Yes Yes Yes
License Keys Protection ua No Yes Yes
conid No Yes Yes
maxip No Yes Yes
maxu No Yes Yes
Concurrent streams uid No No Yes
climit No No Yes
cbeh No No Yes
sid No No Yes
Generic Concurrent streams uid No No Yes
climit No No Yes
sid No No Yes
Device registration uid No No Yes
dlimit No No Yes

Generate a token

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

An Epoch & Unix timestamp conversion tool may be helpful when working with time fields.

Example bash script:

Example script to generate the JWT token:

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

payload='{
  "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 hour. Note: 3600 seconds = 1 hour
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.

Static URL Delivery

Request playback:

curl -X GET \
https://edge.api.brightcove.com/playback/v1/accounts/{{account_id}}/videos/{{video_id}}/master.m3u8?bcov_auth={jwt}

For a list of Static URL endpoints, see the Static URL Delivery document.

Playback Restrictions

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}