Appearance
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.
| Part | Value |
|---|---|
Header alg | HS256. Anything else is refused. |
Header kid | The key's id, starting ctk_. |
sub | Your own id for the viewer, 1 to 255 characters. It must match the customer on the entitlement. |
aud | vabu |
iat and exp | Both required, at most 15 minutes apart, and exp at most 15 minutes ahead. A clock skew of 60 seconds is allowed. |
ctry | Optional. 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.
| Reason | Fix |
|---|---|
missing | Send the token as Authorization: Bearer, beside the publishable key. |
malformed | The token is not a three-part HS256 token with a kid. |
unknown_key | The kid is not a key in this tenant and environment. A token for test mode does not work with a live key. |
key_revoked | The signing key was revoked. Sign with a current one. |
invalid_signature | The secret does not match the key. |
not_yet_valid | iat or nbf is in the future. Check your server's clock. |
token_expired | exp has passed. Sign a new token. |
invalid_claims | aud, 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.