1. Home
  2. Content Management
  3. Content Protection
  4. Content Protection: Token Authentication
  1. Home
  2. Security & Privacy
  3. Content Protection: Token Authentication

Content Protection: Token Authentication


For advanced use cases, use tokens to authenticate viewers accessing your content. This allows for more granular access control than country, domain, or IP rules alone.

When using token protection, your website or application must generate a token. A shared secret derived from a Blue Billywig “Secret” is necessary for token calculation. The shared secret remains known only to the client and Blue Billywig platform, ensuring only authorized websites can calculate valid tokens.

There are two token types:

  • TOTP (RPC Token): a time-based one-time password that rotates automatically; used for simple integrations where you only need time-based access control.
  • JWT: a signed JSON payload with custom claims and expiration; used for advanced integrations requiring per-user or per-content access control.

An interactive JWT demo is available here.

How it works

  1. Your backend retrieves the shared secret (stored securely server-side);
  2. Your backend generates a TOTP or JWT token;
  3. The token is passed to the player via query parameter or HTTP header;
  4. The OVP validates the token against the shared secret;
  5. If valid, video playback is allowed; if invalid, playback is blocked.

Before implementing token-based content protection, ensure you have:

  • A Blue Billywig OVP publication;
  • Access to the OVP management console (specifically: Publication settings > Secrets and Publication settings > Content protection);
  • A Content Protection Policy with a Token rule configured (see 1.0 | Create a Token Secret and 2.0 | Create a Content Protection Policy with a Token Rule);
  • A backend server capable of generating tokens (the shared secret must never be exposed client-side).

1.0 | Create a Token Secret

To create a token:

  • In the OVP sidebar, navigate to Publication settings > Secrets;
  • On the Secrets overview page, click the Create new content protection token button.

  • Fill in the form:
    • Label (required): a descriptive name for your secret (e.g., “Production JWT Token”).
    • Description (optional): additional context about the secret’s purpose.
    • Expiration settings: set the token validity duration using the Days, Hours, and Minutes fields. The expiration must be at least 1 minute. Once the token expires, it becomes invalid and a new token needs to be generated.
    • Click Save & Activate to create and immediately activate the secret, or click Save to create it in an inactive state.
    • After saving, the secret is generated. You can find it in the Secret field on the detail page (click Show secret to reveal the value).

  • The Details panel on the right shows:
    • ID: the numeric secret identifier (you will need this for TOTP tokens).
    • Secret type: confirms “Content protection”.
    • Label, Created, and Modified metadata.
  • Copy the secret value and store it securely in your backend (e.g., in environment variables or a secrets manager).

Important: The shared secret must never be exposed in client-side code, HTML source, or browser-accessible locations.

Note:

Playback is not interrupted when a token expires mid-session. A new token is only required when the page is reloaded.

Note:

To deactivate or delete a secret, open it from the Secrets list and use the Deactivate or Delete actions at the bottom of the detail page.

2.0 | Create a Content Protection Policy with a Token Rule

  • In the OVP sidebar, navigate to Publication settings > Content protection;
  • On the Content protection policies overview page, click Create new content protection policy;
  • Under Policy name, fill in a Title and optional Description;
  • Under Select policy type, configure the rules:
    • Click the Add rule dropdown and select Token;
    • Choose an operator (e.g., is, is not, is empty, is not empty);
    • If using is or is not, select the content protection token you created in 1.0 | Create a Token Secret.
  • You can combine multiple rules:
    • Click Add rule to add an additional condition within the same rule set (AND logic);
    • Click Add rule set to add an alternative set of conditions (OR logic).
  • Click Save to create the policy.

Blocked Content Display

When a token is invalid or missing, the policy determines how blocked content appears to viewers:

  • Media clips: either show the player with playback disabled, or hide the content entirely (a background color is displayed in its place).
  • Channels: either show the channel with playback disabled, or hide it completely (a blank screen appears).

These display options are configured when creating the policy (see Blocked Content Display in the Content Protection Policies article).

Apply the Policy

After creating the policy, assign it to your content:

  • Media clips: navigate to Media library > Media clips, open a clip, go to the Content Protection tab, and select your policy.
  • Channels: navigate to Media library > Channels, open a channel, go to the General tab, scroll to Content Protection, and select your policy.

Important: Applying content protection to a channel will prevent the channel from being displayed. However, the media clips it contains are not automatically protected as well. You must apply the policy to individual media clips separately if they also need to be protected.

3.0 | TOTP Token

Authenticating a request using a TOTP token requires two elements:

  • The ID of the Secret;
  • The generated token itself.

TOTP (Time-based One-Time Password) tokens use the RFC 6238 standard. The token value rotates automatically based on the configured time step (expiration window).

Token Format

The generated token must use a time-step duration matching the secret’s expiration time in seconds. Append the generated token to the ID with a hyphen:

{SECRET_ID}-{GENERATED_TOTP_CODE}

For example: 42-839205. Include this string in the rpctoken header or query parameter.

Generating a TOTP Token

Node.js (using speakeasy)

const speakeasy = require("speakeasy");
const SECRET_ID = "42";
const SHARED_SECRET = "your-shared-secret";
const EXPIRATION = 86400; // 24 hours in seconds (must match OVP setting)

function generateRpcToken() {
  const totp = speakeasy.totp({ secret: SHARED_SECRET, digits: 6, step: EXPIRATION });
  return `${SECRET_ID}-${totp}`;
}
const token = generateRpcToken(); // e.g. "42-839205"

Python (using pyotp)

import pyotp
import base64

SECRET_ID = "42"
SHARED_SECRET = "your-shared-secret"
EXPIRATION = 86400  # 24 hours in seconds

def generate_rpc_token():
    # pyotp expects a base32-encoded secret
    secret_b32 = base64.b32encode(SHARED_SECRET.encode()).decode()
    totp = pyotp.TOTP(secret_b32, digits=6, interval=EXPIRATION)
    code = totp.now()
    return f"{SECRET_ID}-{code}"

token = generate_rpc_token()  # e.g. "42-839205"

PHP (using OTPHP)

<?php
use OTPHP\TOTP;

$secretId = '42';
$sharedSecret = 'your-shared-secret';
$expiration = 86400; // 24 hours

function generateRpcToken(string $secretId, string $secret, int $expiration): string {
    $totp = TOTP::createFromSecret($secret);
    $totp->setPeriod($expiration);
    $totp->setDigits(6);
    $code = $totp->now();
    return "{$secretId}-{$code}";
}

$token = generateRpcToken($secretId, $sharedSecret, $expiration); // e.g. "42-839205"

Passing the TOTP Token

The token can be sent as a query parameter or HTTP header:

  • Query parameter (appended to the embed URL): https://{publication}.bbvms.com/p/{playout}/c/{clipId}.js?rpctoken=42-839205
  • HTTP header: rpctoken: 42-839205

4.0 | JWT Token

When authenticating a request using a JWT token, sign the payload with the shared secret. JWT tokens offer more flexibility than TOTP: you can include custom claims such as expiration time, subject identifiers, and entity-level access restrictions.

JWT Structure

The JWT must be signed using HMAC SHA-256 (HS256) with the shared secret. The payload should include at minimum:

{
  "sub": "user-identifier",
  "iat": 1700000000,
  "exp": 1700000900
}
ClaimDescriptionRequired
subSubject identifier (e.g., user ID or session ID)Recommended
iatIssued-at timestamp (Unix epoch seconds)Recommended
expExpiration timestamp (Unix epoch seconds)Yes
entity_accessEntity-level access restrictions (see 6.0 | Entity-Level Access Control)Optional
Note:

The exp claim must fall within the secret’s configured expiration window.

Generating a JWT Token

Node.js (using jsonwebtoken)

const jwt = require("jsonwebtoken");
const SHARED_SECRET = "your-shared-secret";

function generateJwtToken(userId) {
  const now = Math.floor(Date.now() / 1000);
  const payload = { sub: userId, iat: now, exp: now + 900 }; // 15 minutes
  return jwt.sign(payload, SHARED_SECRET, { algorithm: "HS256" });
}

const token = generateJwtToken("user-123");

Python (using PyJWT)

import jwt
import time

SHARED_SECRET = "your-shared-secret"

def generate_jwt_token(user_id: str) -> str:
    now = int(time.time())
    payload = {
        "sub": user_id,
        "iat": now,
        "exp": now + 900,  # 15 minutes
    }
    return jwt.encode(payload, SHARED_SECRET, algorithm="HS256")

token = generate_jwt_token("user-123")

PHP (using firebase/php-jwt)

<?php
use Firebase\JWT\JWT;

$sharedSecret = 'your-shared-secret';

function generateJwtToken(string $userId): string {
    global $sharedSecret;
    $now = time();
    $payload = [
        'sub' => $userId,
        'iat' => $now,
        'exp' => $now + 900, // 15 minutes
    ];
    return JWT::encode($payload, $sharedSecret, 'HS256');
}

$token = generateJwtToken('user-123');

Passing the JWT Token

The calculated JWT can be provided via the Authorization header as a Bearer token, or through the jwt header or query parameter. These are checked in the following order:

  • Query parameter (appended to the embed URL): https://{publication}.bbvms.com/p/{playout}/c/{clipId}.js?jwt=eyJhbGciOi...
  • HTTP header jwt: jwt: eyJhbGciOi...
  • Authorization Bearer header: Authorization: Bearer eyJhbGciOi...

5.0 | Embedding with Tokens

In a typical integration, your backend generates the token and appends it as a query parameter to the embed URL. Blue Billywig supports several embed methods (read more about embedding). Below are the most common approaches with token authentication.

JavaScript Embed

Append the token as a query parameter to the .js embed URL:

<!-- TOTP -->
<script
  type="text/javascript"
  src="https://{publication}.bbvms.com/p/{playout}/c/{clipId}.js?rpctoken=42-839205"
  async="true"
></script>

<!-- JWT -->
<script
  type="text/javascript"
  src="https://{publication}.bbvms.com/p/{playout}/c/{clipId}.js?jwt=eyJhbGciOi..."
  async="true"
></script>

iFrame Embed

Append the token to the .html embed URL in the src attribute:

<iframe
  onload="
  this.src +=
'#!referrer=' + encodeURIComponent(location.href) + '&realReferrer=' + encodeURIComponent(document.referrer)"
  src="https://{publication}.bbvms.com/p/{playout}/c/{clipId}.html?inheritDimensions=true&jwt=eyJhbGciOi..."
  width="720"
  height="405"
  frameborder="0"
  allowfullscreen
  allow="autoplay; fullscreen"
></iframe>

Server-Side Token Injection

For maximum security, have your backend generate the token and inject it into the embed URL so that the token never appears in static HTML source code:

// Express.js example: renders a page with the token embedded in the player URL
const jwt = require("jsonwebtoken");

app.get("/video/:clipId", (req, res) => {
  const token = generateJwtToken(req.user.id);
  const embedUrl = `https://${PUBLICATION}.bbvms.com/p/${PLAYOUT}/c/${req.params.clipId}.js?jwt=${token}`;
  res.send(`
<html>
  <body>
    <script type="text/javascript" src="${embedUrl}" async="true"></script>
  </body>
</html>
`);
});
Note:

The token is appended as a regular query parameter alongside any other override parameters (e.g., ?jwt=eyJ...&autoPlay=true).

6.0 | Entity-Level Access Control (JWT)

JWT tokens support fine-grained access control through the entity_access claim. This allows you to restrict a token to specific media clips, clip lists, or projects.

Claim Structure

The entity_access claim is an object where keys are entity types and values are arrays of allowed entity IDs:

{
  "sub": "user-123",
  "iat": 1700000000,
  "exp": 1700000900,
  "entity_access": {
    "mediaclip": ["12345", "67890"],
    "mediacliplist": ["101"],
    "project": ["42"]
  }
}
Entity TypeDescription
mediaclipIndividual video clips
mediacliplistClip lists / playlists
projectProjects

Behavior

  • If the entity_access claim is omitted or empty, the token grants access to all content protected by the policy (no entity-level restriction).
  • If entity_access is present, the OVP checks whether the requested content matches one of the allowed entities. Access is granted if any entity type/ID pair matches.
  • An empty instances array ("mediaclip": []) matches all instances of that entity type.

Example: Restricting to a Single Clip

const jwt = require("jsonwebtoken");

function generateClipToken(userId, clipId) {
  const now = Math.floor(Date.now() / 1000);
  const payload = {
    sub: userId,
    iat: now,
    exp: now + 900,
    entity_access: { mediaclip: [String(clipId)] }
  };
  return jwt.sign(payload, SHARED_SECRET, { algorithm: "HS256" });
}
// This token only grants access to clip 12345
const token = generateClipToken("user-123", 12345);

Was this article helpful?

Related Articles

Contact Support
Can't find the answer you're looking for?
Contact Support