Skip to content

Customer tokens ​

A customer token lets your own login stay the only login. Your server knows who is signed in. It signs a short token that says "this is user 42". The browser sends that token to Vabu with a publishable key, and Vabu opens the viewing session for user 42. Vabu never asks your viewer to sign in again, and your secret key never reaches the browser.

Make a signing key ​

Create a key once. Only a secret key can do this.

http
POST /v1/access/customer_token_keys

{ "name": "backend" }
json
{
  "data": {
    "id": "ctk_01J9ZQ4M8T3K6Y2E5W7N0B1C9D",
    "object": "customer_token_key",
    "name": "backend",
    "status": "active",
    "livemode": true,
    "secret": "cts_..."
  }
}

The secret is shown once. Keep it in your server's secret store. Keys are per environment: a token signed for test mode does not work with a live publishable key. Rotate by making a second key, deploying it, then revoking the first with DELETE /v1/access/customer_token_keys/{id}. Revoking stops new sessions at once.

The token ​

A token is a standard JWT signed with HMAC-SHA256, so a few lines in any language make one.

PartValue
Header algHS256. Anything else is refused.
Header kidThe key's id, starting ctk_.
subYour own id for the viewer, 1 to 255 characters. It must match the customer on the entitlement.
audvabu
iat and expBoth required, at most 15 minutes apart, and exp at most 15 minutes ahead. A clock skew of 60 seconds is allowed.
ctryOptional. A two-letter country code you vouch for, used for country limits.

Sign a fresh token each time a page loads. Ten minutes is a good lifetime.

js
import { createHmac } from 'node:crypto'

const b64 = (value) => Buffer.from(JSON.stringify(value)).toString('base64url')

export function customerToken(customer, keyId, secret) {
  const now = Math.floor(Date.now() / 1000)
  const head = b64({ alg: 'HS256', typ: 'JWT', kid: keyId })
  const body = b64({ sub: customer, aud: 'vabu', iat: now, exp: now + 600 })
  const signature = createHmac('sha256', secret).update(`${head}.${body}`).digest('base64url')
  return `${head}.${body}.${signature}`
}
php
<?php

function b64url(string $data): string
{
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function customerToken(string $customer, string $keyId, string $secret): string
{
    $now = time();
    $head = b64url(json_encode(['alg' => 'HS256', 'typ' => 'JWT', 'kid' => $keyId]));
    $body = b64url(json_encode(['sub' => $customer, 'aud' => 'vabu', 'iat' => $now, 'exp' => $now + 600]));
    $signature = b64url(hash_hmac('sha256', "$head.$body", $secret, true));

    return "$head.$body.$signature";
}
python
import base64
import hashlib
import hmac
import json
import time


def b64url(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()


def customer_token(customer: str, key_id: str, secret: str) -> str:
    now = int(time.time())
    head = b64url(json.dumps({"alg": "HS256", "typ": "JWT", "kid": key_id}, separators=(",", ":")).encode())
    body = b64url(json.dumps({"sub": customer, "aud": "vabu", "iat": now, "exp": now + 600}, separators=(",", ":")).encode())
    signature = b64url(hmac.new(secret.encode(), f"{head}.{body}".encode(), hashlib.sha256).digest())
    return f"{head}.{body}.{signature}"
go
package vabu

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"time"
)

// CustomerToken signs a short-lived token for one viewer.
func CustomerToken(customer, keyID, secret string) string {
	enc := base64.RawURLEncoding
	now := time.Now().Unix()

	head, _ := json.Marshal(map[string]any{"alg": "HS256", "typ": "JWT", "kid": keyID})
	body, _ := json.Marshal(map[string]any{"sub": customer, "aud": "vabu", "iat": now, "exp": now + 600})

	unsigned := enc.EncodeToString(head) + "." + enc.EncodeToString(body)
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(unsigned))

	return unsigned + "." + enc.EncodeToString(mac.Sum(nil))
}

Use it in the browser ​

Send the publishable key and the token. Leave customer out of the body: with a publishable key the customer is always the token's sub.

http
POST /v1/streams/playback_tokens
Vabu-Publishable-Key: vpk_live_...
Authorization: Bearer <customer token>

{ "resource": "ls_...", "device_id": "browser-1" }

These calls accept a publishable key with a customer token: open a session (POST /v1/streams/playback_tokens or POST /v1/access/sessions), heartbeat and end. A heartbeat or an end only acts on the token's own sessions. Everything else, including entitlements and access checks, needs a secret key.

Refresh the token before it expires. With a publishable key every call, heartbeats included, is checked against the token. When it expires, heartbeats fail with 401 invalid_customer_token and the session lapses. Ask your server for a new token before the old one runs out, or fetch one for each call.

Errors ​

A rejected token returns 401 invalid_customer_token, and error.details.reason says why.

ReasonFix
missingSend the token as Authorization: Bearer, beside the publishable key.
malformedThe token is not a three-part HS256 token with a kid.
unknown_keyThe kid is not a key in this tenant and environment. A token for test mode does not work with a live key.
key_revokedThe signing key was revoked. Sign with a current one.
invalid_signatureThe secret does not match the key.
not_yet_validiat or nbf is in the future. Check your server's clock.
token_expiredexp has passed. Sign a new token.
invalid_claimsaud, sub, iat or exp is wrong, or the lifetime is over 15 minutes.

Why HMAC ​

The signer needs no library and no key format beyond HMAC. The secret lives in two places only: your server, and Vabu, where it is encrypted at rest. If you use Vabu ID for login instead, see sign-in options.

Vabu OS documentation