Skip to content

Access tokens

media.access.token

Use viewer JWTs for new integrations. Your application checks the viewer's permissions and obtains a signed playback token through the central administration API. The player presents this token to the streamer, which validates it locally without a request to your application or an HTTP authorization backend.

One JWT works on every edge in the cluster that has received the corresponding public key. Moving to another edge does not require a new token. This admission method eliminates the authorization request load when many viewers start watching together and removes the dependency of each viewer's admission on the availability and latency of an external HTTP backend.

The HTTP authorization backend is a legacy mechanism, deprecated and not recommended for new integrations. It remains available for compatibility.

How the token is passed

A token can be supplied in two ways:

  • URL parameter — ?token=<token>, which takes precedence.
  • Header — Authorization: Bearer <token>, used when the URL has no token parameter.

For HLS, open the master playlist with the token: Sapsan adds it to nested playlist and media URLs. If you use a header, the client must send it with protected requests. Never give the administration key to the player: only your application needs it to issue JWTs.

Obtain a JWT through central

Create a stream named news and connect the streamers to central first. Wait for configuration and public keys to reach the edges. This example requires curl and jq; replace the domains and administration key with your own values.

CENTRAL_URL='https://central.example.com'
EDGE_1='https://edge1.example.com'
EDGE_2='https://edge2.example.com'
ADMIN_KEY='replace-with-management-api-key'

ISSUED=$(curl --fail-with-body --silent --show-error \
  -X POST "$CENTRAL_URL/central/api-v4/play-sessions/grants" \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"streams":["news"],"user_id":"viewer-42","ttl_secs":3600,"max_concurrent":1}')

TOKEN=$(printf '%s' "$ISSUED" | jq -er '.token')
JTI=$(printf '%s' "$ISSUED" | jq -er '.jti')

Central creates a grant, a record of the access issued, and returns 201 Created. Grant fields and token are at the same level in the response. jti identifies the grant, and expires_at is its expiry time in UTC. The JWT itself is returned only on creation; reading the grant through the API does not return it.

Request fields:

Field Meaning
streams A nonempty list of exact permitted media names. Use news for the stream news; wildcards are not supported.
user_id The viewer's identifier in your system. Included in session statistics.
ttl_secs Lifetime in seconds from issuance. Defaults to 14400, or four hours.
max_concurrent Maximum concurrently active sessions for this grant. Omitted means 1; null means unlimited; 0 is rejected.
bind_ip Optional binding to an IP address or CIDR. Without it, the token permits the client's address to change.
note Optional operator note.

Playback and moving between edges

Retrieve the HLS playlist from the first edge, then the second, using the same TOKEN:

curl --fail-with-body --silent --show-error \
  "$EDGE_1/streaming/v/news/index.m3u8?token=$TOKEN"

curl --fail-with-body --silent --show-error \
  "$EDGE_2/streaming/v/news/index.m3u8?token=$TOKEN"

These commands retrieve playlists; for playback, open the corresponding URL in an HLS player. Both edges must be able to serve the stream news. JWT validation alone does not place the stream or make its source available.

To let the balancer select a streamer, use https://central.example.com/playback/v/news/index.m3u8?token=<JWT>. This URL also depends on central being available: it selects a streamer and redirects the request or proxies the media. A direct edge URL is validated by the streamer itself.

Moving to another edge may create a new local session. Central associates sessions with their grant and tracks activity so that an old edge that no longer receives media requests does not occupy the limit indefinitely. See cluster sessions and moving between edges.

Scope and lifetime

The MoQ listener does not currently use this authorization mechanism.

The JWT is signed with Ed25519 using the EdDSA algorithm; kid in the header selects the public key. Its payload contains:

  • jti — grant identifier.
  • user_id — viewer identifier.
  • exp — expiry in Unix seconds.
  • streams — permitted media names.
  • bind_ip — IP address or network, if binding is configured.

Live and archive playback of a stream use the same name: news. For VOD, specify vod/<asset-id>: the grant covers that asset's files regardless of the storage root or filename. The standard aud claim is not used for this scope. The max_concurrent limit is stored in central's registry, outside the JWT.

The edge validates the signature, lifetime and scope locally; an IP binding is also checked locally. A 30-second allowance covers clock skew. An invalid JWT is rejected without attempting HTTP backend authorization. After expiry, subsequent HTTP requests are rejected; extending access requires a new grant. An already open continuous HTTP response is not guaranteed to stop immediately; see this limitation in session authorization. Plan token refresh in your application if viewing may outlast the token.

Session limits and revoking access

max_concurrent applies to the grant across the cluster. Central receives session information and asynchronously terminates the oldest active sessions above the limit; the new session remains. This does not instantly reject the second request at every edge. The limit counts server sessions, not necessarily distinct physical devices: requests with the same media, IP and grant may merge into one HTTP session.

Change the limit without reissuing the JWT:

curl --fail-with-body --silent --show-error \
  -X PATCH "$CENTRAL_URL/central/api-v4/play-sessions/grants/$JTI" \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"max_concurrent":2}'

Revoke the entire grant:

curl --fail-with-body --silent --show-error \
  -X DELETE "$CENTRAL_URL/central/api-v4/play-sessions/grants/$JTI" \
  -H "Authorization: Bearer $ADMIN_KEY"

Revocation returns 204 No Content. Central delivers it to streamers through a separate termination channel. Once a streamer receives the revocation, it closes the grant's sessions and rejects new requests. Revocation is not an immediate shared check on every media request: an edge disconnected from central cannot learn about a new revocation until the channel recovers.

Key delivery and operation without central connectivity

Central keeps the private signing key and delivers public keys to streamers in auth.ticket_keys with their configuration. When no key list is specified in the policy, central inserts its own keys automatically. An edge with a public key can validate JWTs but cannot issue new tokens.

Validation on an already configured edge does not require central to be available. While the JWT is valid and the media is available, local admission continues using the keys already received. These operations still depend on central:

  • Issuing new grants — requires the central API and its storage.
  • New keys and policy changes — must reach the streamers.
  • Revocations and the shared session limit — require the termination channel and session information.
  • Balancing through /playback — is handled by central.

During rotation, the old public key must remain available to validate JWTs that are still valid. Do not remove it before those tokens expire. Support for multiple keys does not imply a public API for automatic rotation.

Static tokens

Static tokens remain available for compatibility and simple existing installations. They do not replace issuing individual grants to viewers:

auth:
  play_tokens:
  - viewer-secret-1
  publish_tokens:
  - publisher-secret-1
  • play_tokens — playback tokens.
  • publish_tokens — publishing tokens.

A viewer JWT is intended for playback; do not use it as publishing permission. In a managed cluster, central delivers the auth policy; the YAML above shows the settings structure for a standalone streamer.

opaque_tokens defaults to true for compatibility. After migration, set it to false to disable the general opaque-token and HTTP backend path, including static tokens in the global policy. Playback tokens in individual stream configurations continue to work. Explicit open_playback still opens playback access. Check the entire access policy during migration.