JSON Web Token (JWT) を作成する

このトピックでは、Brightcove の再生制限で使用できる JSON Web Token (JWT) の作成方法について説明します。

はじめに

動画ライブラリへのアクセスをさらに保護したり、コンテンツにユーザー単位の制限を適用したりするために、Brightcove Playback API の呼び出しに JSON Web Token (JWT) を渡すことができます。

JWT に初めて触れる方は、以下を確認してください:

ワークフロー

JSON Web Token (JWT) を作成して Brightcove に登録するには、次の手順に従ってください:

  1. 公開鍵と秘密鍵のペアの生成
  2. 公開鍵を Brightcove に登録する
  3. JSON Web Token を作成する
  4. 再生をテストする

公開鍵と秘密鍵のペアを生成する

公開鍵と秘密鍵のペアを生成し、公開鍵を Brightcove に提供します。秘密鍵はトークンに署名するために使用され、Brightcove とは共有されません。

公開鍵と秘密鍵のペアを生成する方法はいくつかあります。以下にいくつか例を挙げます:

Bash スクリプトの例:

鍵ペアを生成するスクリプトの例:

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

スクリプトを実行します:

$ bash keygen.sh
Goを使用した例

Goプログラミング言語を使用してキーのペアを生成する例です:

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

Node.jsを使用した例

Node.jsを使用してキーのペアを生成する例です:

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

公開鍵の登録

秘密鍵は自分が所有し、これを使用して署名済みトークンを生成します。公開鍵は、トークンを検証するために、Brightcove と共有します。キーAPIを使用して、公開鍵を Brightcove に登録することができます。

APIの詳細については、認証 API の使用のドキュメントをご覧ください。

JSON Web Tokenの作成

発行者はJSON Web Token (JWT) を作成します。このトークンは、SHA-256ハッシュアルゴリズムを使用したRSAアルゴリズムで署名されます(JWT仕様では"RS256"と記載されています)。他のJWTアルゴリズムはサポートされません。

JWTの標準的なクレームのサブセットと、Brightcoveが定義するプライベートクレームの一部を使用します。秘密鍵で署名された JSON Web Token を作成します。

JWTトークンを生成するためのライブラリは一般的に利用可能です。詳細については、JSON Web Tokensのサイトをご覧ください。

時間フィールドを扱う際には、Epoch & Unix タイムスタンプ変換ツールが便利です。

例: bashスクリプト

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"

スクリプトを実行します:

$ bash jwtgen.sh

Go を使用した例

以下は、サードパーティ- ライブラリを使用せずにトークンを生成するための(cliツールとしての)リファレンス Go 実装の例になります:

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

結果

以下は、https://JWT.io を使用して、すべてのクレームを指定したデコード済みトークンの例です:

ヘッダー:

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

ペイロード:

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

再生テスト

必須ではありませんが、プレーヤーを設定する前に、ビデオの再生をテストしたい場合があるかと思います。

静的URL配信

再生リクエスト:

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

静的URLエンドポイントの一覧については、静的URL配信 のドキュメントを参照してください。

再生制限

再生リクエスト:

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

静的URL配信のクレーム

以下のクレームは Brightcove の静的 URL 配信で使用できます。

クレーム タイプ 必須 説明
accid 文字列 再生されるコンテンツを所有するアカウントID
iat 整数 このトークンが発行された時刻(エポック秒)
exp 整数 このトークンが無効になる時刻(エポック秒)。iatから最大30日以内である必要があります。
drules 文字列配列 適用する配信ルールアクションIDのリスト。詳細については、配信ルールの実装ドキュメントを参照してください。
config_id クエリパラメータが設定されている場合でも、このクレームによってオーバーライドされるため、このパラメーターは無視されます。
conid 文字列 存在する場合、このトークンは特定の Video Cloud 動画ID のみを認証します。これは、DRM/HLSeストリームまたは非DRMアセットのいずれかを指定できます。

有効な動画IDである必要があります。参照IDはサポートされていません。
pro 文字列 1つのビデオに複数のプロテクション タイプが存在する場合に指定するプロテクション タイプ。

値:
  • ""(クリアコンテンツのデフォルト)
  • "aes128"
  • "widevine"
  • "playready"
  • "fairplay"
vod オブジェクト Video-On-Demandの特定の設定オプションを含みます。
vod.ssai 文字列 サーバーサイド広告挿入(SSAI)設定ID。このクレームは HLS または DASH VMAP を取得するために必要です。
aud 文字列 はい* JWTが意図されている受信者(オーディエンス)。

* 現在、このクレームはオプションですが、将来的に強制される予定です。 aud クレームがない JWTトークンは Playback API によって拒否されます。aud クレームが存在する場合、static.api.brightcove.com という値が含まれている必要があります。


以下は、使用可能な JSON Web Token(JWT)のクレームの例です:

{
        // アカウントID:このアカウントにのみ有効なJWT
        "accid":"4590388311111",
        // 発行時刻:JWTが作成されたタイムスタンプ
        "iat":1575484132,
        // 有効期限:JWTが期限切れになるタイムスタンプ
        "exp":1577989732,
        // 配信ルール:適用される配信ルールIDのリスト
        "drules": ["0758da1f-e913-4f30-a587-181db8b1e4eb"],
        // コンテンツID:JWTが有効な動画ID
        "conid":"5805807122222",
        // プロテクション:1つのビデオに複数の保護タイプが存在する場合の指定
        "pro":"aes128",
        // VODの特定の設定オプション
        "vod":{
        // 適用するSSAI構成
        "ssai":"efcc566-b44b-5a77-a0e2-d33333333333"
    }
}

再生制限のためのクレーム

以下のクレームは、Brightcove 再生制限と共に使用することができます。再生制限の一環として、以下を実装することができます:

機能 クレーム タイプ 機能に必須 DRMのみ 説明
一般 accid 文字列 はい 再生されるコンテンツの所有者のアカウントID
aud 文字列の配列 JWTの対象受信者(オーディエンス)。

このクレームはオプションです。aud クレームがない JWTトークンも Playback API に送信して動作します。

aud クレームが存在する場合、値として playback.api.brightcove.com を含む必要があります。
exp 整数 はい トークンの有効期限(エポックからの秒数)。発行時間(iat)から30日以内である必要があります。
nbf 整数 トークンの有効開始時間(エポックからの秒数)。指定がない場合は即時有効。
iat 整数 はい トークンの発行時間(エポックからの秒数)
ip 文字列 地理的制限を評価するためのクライアントIPを上書きできます。有効な IPv4(短縮形なし)または IPv6 形式である必要があります。

例:Brightcove Playback APIの前にプロキシがある場合、ip クレームをエンドユーザーのIPアドレスに設定することで、プロキシのIPではなくエンドユーザーのIPを使用して地理的制限を評価できます。
再生権利 prid 文字列 この動画のカタログに設定されているIDを上書きするための playback_rights_id

このフィールドは検証されません。

tags 文字列の配列 存在する場合、このトークンは、リストされたタグ値を持つ動画にのみ有効です。これらの動画のみが再生が許可されます。
vids 文字列の配列 存在する場合、トークンは一連の動画IDに対してのみライセンス取得が許可されます。

ライセンスキーの保護 ua 文字列 存在する場合、このトークンは、User-Agent からのリクエストにのみ有効です。

このフィールドは特定のフォーマットに従う必要はありません。
ライセンスキーの保護 が有効である必要があります。
conid 文字列 存在する場合、このトークンは、特定の Video Cloud 動画IDに対してのみライセンス取得が許可されます。

有効な動画IDである必要があります。
ライセンスキーの保護 が有効である必要があります。
maxip 整数 はい 存在する場合、このトークンは、この数の異なるIPアドレスのみで使用可能です。

セッション トラッキングには必須です。HLSe(AES-128)のみ。
ライセンスキーの保護 が有効である必要があります。
maxu 整数 はい 存在する場合、このトークンは、この数のライセンスリクエストに対してのみ有効です。

  • HLSeの場合、動画を再生する際に通常は複数のリクエストが行われます(通常はレンディションごとに1回)。maxu はこれらの追加リクエストを考慮して十分に高く設定する必要があります。
セッション トラッキングには必須です。HLSe(AES-128)のみ。
ライセンスキーの保護 が有効である必要があります。
同時ストリーム uid 文字列 はい はい 最終視聴者のユーザーID。このフィールドは複数のセッションを関連付け、ストリーム同時実行を管理するために使用されます。

任意のID(最大64文字、A-Z、a-z、0-9、および =/,@_.+- の文字制限あり)を使用できますが、用途に応じて、Brightcove はセッションをユーザーごとに追跡するためのユーザー識別子、または有料アカウントごとのセッションを追跡するためのアカウント識別子の使用を推奨しています。

セッション同時実行の場合、必須
climit 整数 はい はい このフィールドが含まれる場合、ライセンス更新リクエストとともにストリーム同時実行チェックが有効になります。この値は許可される同時視聴者数を示します。

セッション同時実行の場合、必須
cbeh 文字列 はい 値を BLOCK_NEW に設定すると、同時ストリーム制限が有効になり、ストリームの最大数に達したときに、同じユーザーからのリクエストであっても、新しいリクエストがブロックされます。

BLOCK_NEW_USER に値を設定すると、ストリームの最大数に達したときに、新規ユーザーからの新規リクエストのみをブロックします。

デフォルトでは、最大ストリーム数に達した場合、最も古いストリームをブロックします。
sid 文字列 はい 現在のストリームのセッションIDを指定すると、セッションの定義方法を制御できます。デフォルトでは、セッションは User-Agent(ブラウザ)+ IPアドレス + ビデオID によって定義されます。

例えば、セッションの定義を「IPアドレス + ビデオID」に緩和することができます。

デバイス制限 uid 文字列 はい はい 最終視聴者のユーザーID。このフィールドは複数のセッションを関連付け、ストリーム同時実行を管理するために使用されます。

任意のID(最大64文字、A-Z、a-z、0-9、および =/,@_.+- の文字制限あり)を使用できますが、用途に応じて、Brightcove はセッションをユーザーごとに追跡するためのユーザー識別子、または有料アカウントごとのセッションを追跡するためのアカウント識別子の使用を推奨しています。

デバイス登録の場合、必須
dlimit 整数 はい はい このフィールドが含まれる場合、指定されたユーザー(uid)に関連付けられるデバイスの数を制御します。値は 0 より大きい必要があります。

従来許可されたデバイスは、後のリクエストで dlimit の値が下げられても引き続き動作します。

例: 値が 3 に設定されている場合、ユーザーはデバイスA、B、Cで再生可能です(すべて許可されます)。デバイスDでの再生は拒否されます。

値が 1 に変更された場合、Playback Rights API を使用してデバイスを管理することによってデバイスが手動で無効にされない限り、ユーザーは3つのデバイスA、B、Cすべてで再生することができます。

デバイス登録の場合、必須
配信ルール drules 文字列 適用する配信ルールのアクションID。詳細については、配信ルールの実装ドキュメントをご覧ください。

階層別クレーム

再生制限のために利用可能な複数のセキュリティパッケージが用意されています。詳細については、概要: Brightcove 再生制限ドキュメントを参照してください。

以下は、各再生制限パッケージで利用可能なクレームです:

機能 クレーム セキュリティ層1 セキュリティ層2 セキュリティ層3
一般 accid はい はい はい
aud はい はい はい
iat はい はい はい
exp はい はい はい
nbf はい はい はい
再生権限 [1] prid はい はい はい
tags はい はい はい
vids はい はい はい
ライセンスキー保護 ua いいえ はい はい
conid いいえ はい はい
maxip いいえ はい はい
maxu いいえ はい はい
同時ストリーム uid いいえ いいえ はい
climit いいえ いいえ はい
cbeh いいえ いいえ はい
sid いいえ いいえ はい
汎用同時ストリーム uid いいえ いいえ はい
climit いいえ いいえ はい
sid いいえ いいえ はい
デバイス登録 uid いいえ いいえ はい
dlimit いいえ いいえ はい