Création d'un jeton Web JSON (JWT)

Dans cette rubrique, vous allez apprendre à créer un fichier JSON Web Token(JWT) utilisable avec les restrictions de lecture de Brightcove.

Introduction

Pour ajouter un niveau de protection supplémentaire lors de l'accès à votre vidéothèque ou pour appliquer des restrictions au niveau utilisateur à votre contenu, vous pouvez passer un JSON Web Token (JWT) avec votre appel à l'API Brightcove Playback.

Si vous êtes nouveau JWT's, consultez les points suivants :

Flux de travail

Pour créer un compte JSON Web Token (JWT) et vous enregistrer auprès de Brightcove, procédez comme suit :

  1. Générer une paire de clés publique-privée
  2. Enregistrer la clé publique avec Brightcove
  3. Créer un JSON Web Token
  4. Tester la lecture

Générer une paire de clés publique-privée

Vous (l'éditeur) allez générer une paire de clés publique-privée et fournir la clé publique à Brightcove. Vous utiliserez la clé privée pour signer des jetons. La clé privée n'est pas partagée avec Brightcove.

Il existe de nombreuses façons de générer la paire de clés publique-privée. Voici quelques exemples:

Exemple de script bash :

Exemple de script pour générer la paire de clés :

#!/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"

Exécutez le script :

$ bash keygen.sh
Exemple d'utilisation Go

Exemple d'utilisation du langage Go de programmation pour générer la paire de clés :

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)
  }
  

Exemple utilisant node.js

Exemple utilisant node.js pour générer la paire de clés :

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");

Enregistrer la clé publique

Vous possédez la clé privée et vous l'utiliserez pour générer des jetons signés. Vous partagerez la clé publique avec Brightcove pour vérifier vos jetons. L'API de clé vous permet d'enregistrer votre clé publique auprès de Brightcove.

Pour plus de détails sur l'API, consultez le Utilisation des API d'authentification document.

Créer un JSON Web Token

Les éditeurs créent un JSON Web Token (JWT). Le jeton est signé avec l'algorithme RSA à l'aide de l'algorithme de hachage SHA-256 (identifié dans la spécification JWT comme "RS256") Aucun autre algorithme JWT ne sera pris en charge.

Un sous-ensemble de la norme JSON Web Token claims sera utilisé, ainsi que certaines revendications privées définies par Brightcove. Vous allez créer une JSON Web Token signature avec votre clé privée.

Réclamations pour la livraison d'URL statiques

Les revendications suivantes peuvent être utilisées avec la diffusion d'URL statiques de Brightcove.

Réclamation Type Obligatoire Description
accid Chaîne L'identifiant du compte qui possède le contenu en cours de lecture
iat Entier Heure à laquelle ce jeton a été émis, en secondes depuis l'époque
exp Entier Temps pendant lequel ce jeton ne sera plus valide, en secondes depuis l'époque. Ne doit pas être plus de 30 jours à compter de iat
drules Chaîne de caractères[] Liste des ID d'action des règles de livraison à appliquer. Pour plus de détails, voir le document Mise en œuvre des règles de livraison .
Si le paramètre de config_id requête est également défini, il sera ignoré, car cette affirmation l'annule.
conid Chaîne S'il est présent, ce jeton n'autorisera qu'un identifiant vidéo spécifique du nuage de vidéos. Il peut s'agir d'un flux DRM/HLSe ou d'un bien non DRM.

Doit être un ID vidéo valide. Notez que l'ID de référence n'est pas pris en charge.
pro Chaîne Spécifie un type de protection dans le cas où plusieurs sont disponibles pour une seule vidéo.

Valeurs :
  • « » (par défaut pour un contenu clair)
  • « aes128"
  • « veuve »
  • « prêt à jouer »
  • « fairplay »
vod Objet Contient des options de configuration spécifiques pour la vidéo à la demande.
vod.ssai Chaîne Votre ID de configuration d'insertion d'annonces côté serveur (SSAI). Cette revendication est requise pour récupérer un HLS ou un DASH VMAP.

Voici un exemple des revendications JSON Web Token (JWT) que vous pourriez utiliser :

{
// 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"
}
}

Réclamations pour restrictions de lecture

Les revendications suivantes peuvent être utilisées avec Restrictions de lecture Brightcove. Dans le cadre des restrictions de lecture, vous pouvez mettre en œuvre les éléments suivants :

Caractéristique Réclamation Type Requis pour la fonctionnalité DRM uniquement Description
Général accid Chaîne Oui L'identifiant du compte qui possède le contenu en cours de lecture
iat Entier Oui Heure à laquelle ce jeton a été émis, en secondes depuis l'époque
exp Entier Oui Non requis, mais fortement recommandé.

Heure à laquelle ce jeton ne sera plus valide, en secondes depuis l'époque. Ne doit pas être plus de 30 jours à compter de iat
nbf Entier Heure à laquelle ce jeton commencera à être valide, en secondes depuis l'époque.
S'il n'est pas spécifié, le jeton est disponible immédiatement.
Droits de lecture prid Chaîne A playback_rights_id utilisé pour remplacer l'identifiant défini dans le catalogue pour cette vidéo

Ce champ n'est pas validé

tags Tableau <chaînes> s'il est présent, ce jeton n'est valide que pour les vidéos qui ont les valeurs de balises répertoriées. Seules ces vidéos sont autorisées pour la lecture.
vids Tableau <chaînes> S'il est présent, ce jeton n'autorisera la recherche de licences que pour un ensemble d'identifiants vidéo.

Protection des clés de licence ua Chaîne S'il est présent, ce token ne sera valable que pour les requêtes de cet User-Agent.

Ce champ n'a pas à suivre un format particulier.
La protection des clés de licence doit être activée.
conid Chaîne S'il est présent, ce jeton n'autorisera la récupération de licence que pour un identifiant vidéo Video Cloud spécifique.

Il doit s'agir d'un identifiant
vidéo valide. La protection des clés de licence doit être activée.
maxip Entier Oui S'il est présent, ce token ne pourra être utilisé que par ce nombre d'adresses IP différentes.

Requis pour le suivi des sessions ; HLSE (AES-128) uniquement
. La protection des clés de licence doit être activée.
maxu Entier Oui S'il est présent, ce jeton ne sera valide que pour ce nombre de demandes de licence.

  • Pour HLSe, les joueurs feront plusieurs demandes lors de la lecture d'une vidéo, généralement une par rendu. Les maxu doit être suffisamment élevé pour tenir compte de ces demandes supplémentaires.
Requis pour le suivi des sessions ; HLSE (AES-128) uniquement
. La protection des clés de licence doit être activée.
Flux simultanés uid Chaîne Oui Oui L'identifiant de l'utilisateur final. Ce champ est utilisé pour mettre en corrélation plusieurs sessions afin d'appliquer la concurrence de flux.

Vous pouvez utiliser un identifiant arbitraire (64 caractères maximum, limités à A-Z, a-z, 0-9, et =/,@_.+-). Cependant, selon votre cas d'utilisation, Brightcove recommande l'utilisation d'un identifiant d'utilisateur pour suivre les sessions par utilisateur ou d'un identifiant de compte pour suivre les sessions par compte payant.

Obligatoire pour la concurrence de session
climit Entier Oui Oui Lorsque ce champ est inclus, il active la vérification de la concurrence Stream ainsi que les demandes de renouvellement de licence. Cette valeur indique le nombre d'observateurs simultanés autorisés.

Obligatoire pour la concurrence de session
cbeh Chaîne Oui Réglez la valeur sur BLOCK_NEW pour permettre aux limites de flux simultanés de bloquer toute nouvelle demande, même du même utilisateur, lorsque le nombre maximum de flux est atteint.

Définissez la valeur sur BLOCK_NEW_USER pour bloquer toute nouvelle demande d'un nouvel utilisateur uniquement lorsque le nombre maximum de flux est atteint.

La valeur par défaut bloque le flux le plus ancien lorsque le nombre maximal de flux est atteint.
sid Chaîne Oui La spécification de l'ID de session du flux actuel vous permet de contrôler la manière dont une session est définie. Par défaut, une session est définie comme User-Agent (navigateur) + adresse IP + identifiant vidéo.

Par exemple, vous pouvez élargir la définition de la session à l'adresse IP + l'identifiant vidéo

Limites de l'appareil uid Chaîne Oui Oui L'identifiant de l'utilisateur final. Ce champ est utilisé pour mettre en corrélation plusieurs sessions afin d'appliquer la concurrence de flux.

Vous pouvez utiliser un identifiant arbitraire (64 caractères maximum, limités à A-Z, a-z, 0-9, et =/,@_.+-). Cependant, selon votre cas d'utilisation, Brightcove recommande l'utilisation d'un identifiant d'utilisateur pour suivre les sessions par utilisateur ou d'un identifiant de compte pour suivre les sessions par compte payant.

Obligatoire pour l'enregistrement de l'appareil
dlimit Entier Oui Oui Lorsque ce champ est inclus, il contrôle le nombre d'appareils pouvant être associés à l'utilisateur spécifié (uid). La valeur doit être > 0.

Les périphériques précédemment autorisés continueront à fonctionner si la dlimit valeur est supprimée dans les requêtes ultérieures.

Exemple : si la valeur est définie sur 3, l'utilisateur peut jouer sur les appareils A, B et C (tout serait autorisé). Essayer de jouer sur l'appareil D serait refusé.

Si la valeur est modifiée 1, l'utilisateur peut toujours jouer sur les 3 appareils A, B et C, sauf si les périphériques sont révoqués manuellement en gérant les appareils avec l' API Droits de lecture.

Obligatoire pour l'enregistrement de l'appareil
Règles de livraison drules Chaîne de caractères[] Liste des ID d'action des règles de livraison à appliquer. Pour plus de détails, voir le document Mise en œuvre des règles de livraison .

Réclamations par niveau

Plusieurs packages de sécurité sont disponibles pour les restrictions de lecture. Pour plus de détails, consultez la présentation : Document sur les restrictions de lecture Brightcove.

Voici les revendications disponibles pour chaque paquet de restrictions de lecture :

Caractéristique Réclamations Niveau de sécurité 1 Niveau de sécurité 2 Niveau de sécurité 3
Général accident Oui Oui Oui
iat Oui Oui Oui
exp Oui Oui Oui
nbf Oui Oui Oui
Droits de lecture [1] prid Oui Oui Oui
tags Oui Oui Oui
vidéos Oui Oui Oui
Protection des clés de licence ua Non Oui Oui
conid Non Oui Oui
maxip Non Oui Oui
maxu Non Oui Oui
Flux simultanés UID Non Non Oui
climat Non Non Oui
cbeh Non Non Oui
sid Non Non Oui
Flux génériques simultanés UID Non Non Oui
climat Non Non Oui
sid Non Non Oui
Enregistrement de l'appareil UID Non Non Oui
dlimit Non Non Oui

Générer un jeton

Les bibliothèques sont généralement disponibles pour générer des jetons JWT. Pour plus de détails, consultez le JSON Web Tokens site.

Un outil de conversion d'horodatage Epoch & Unix peut s'avérer utile lorsque vous travaillez avec des champs temporels.

Exemple de script bash :

Exemple de script pour générer le jeton JWT :

#! /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"

Exécutez le script :

$ bash jwtgen.sh

Exemple d'utilisation Go

Voici un exemple d' Go implémentation de référence (en tant qu'outil cli) pour générer des jetons sans l'utilisation d'une bibliothèque tierce :

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)
}

Résultats

Voici un exemple de jeton décodé utilisant https://JWT.io spécifiant l'ensemble complet des revendications :

ENTÊTE:

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

CHARGE UTILE:

{
  "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"
}

Tester la lecture

Bien que cela ne soit pas obligatoire, vous souhaiterez peut-être tester la lecture vidéo avant de configurer un lecteur.

Livraison d'URL statique

Demander la lecture :

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

Pour une liste des points d'extrémité des URL statiques, voir le document Static URL Delivery .

Restrictions de lecture

Demander la lecture :

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