AccessToken2 Semantics and Privileges
Privilege bits, channel/uid binding, exp vs pexp, wildcard tokens, renewal grace and differences from Agora.
This page covers the semantics of the token itself: what is inside it, who signs it, when it stops being valid, and what happens when someone tries to exceed their privileges. For how to call the issue, renewal and verification endpoints, see Token authentication; the relationship between the two pages is "semantics vs usage".
One-sentence model
A token is a signed snapshot of project App ID + channel name + uid + role privilege set + expiry, HMAC-SHA256-signed with the project's App Certificate. Both the server and the SDKs treat it as an opaque string: they do not decrypt it, do not concatenate into it, and do not cache a parsed result to make authorization decisions.
Wire format
nrt1.<base64url(payload JSON)>.<base64url(HMAC-SHA256(appCertificate, "nrt1.<base64url(payload)>"))>| Segment | Content | Description |
|---|---|---|
| Prefix | nrt1 | Format version; if the structure changes we will bump it to nrt2 and keep the SDKs and the gateway compatible with both for a while |
| payload | base64url(JSON) | Fields in the table below; it is not encrypted, anyone can decode it, so do not put sensitive information in it |
| Signature | base64url(HMAC-SHA256) | Signed with appCertificate; verification uses a constant-time comparison to prevent timing side channels |
Payload fields
These fields are defined by the shared token types and mirrored in the v1 domain IDL:
| Field | Type | Meaning | |
|---|---|---|---|
v | number | Version, currently 1 | |
app | string | Project App ID (32 hex characters) | |
ch | string | Channel name; * means wildcard (any channel) | |
uid | number | Numeric uid, 1..2147483647 | |
role | host \ | audience | Determines the default privilege set |
priv | string[] | The effective privilege bits (see the table below); the only basis for authorization decisions | |
iat | number | Issued at (epoch seconds) | |
exp | number | Token lifetime expiry (epoch seconds) | |
pexp | number | Privilege expiry (epoch seconds), always ≤ exp | |
salt | number | Random number that makes identical inputs produce different tokens (prevents fingerprinting and replay clustering) | |
usr | string? | Identity hint used only for auditing and log correlation, not part of authorization |
Privilege bits
| Privilege bit | Meaning | host | audience |
|---|---|---|---|
join_channel | Allow joining the channel | ✅ | ✅ |
publish_audio | Allow publishing audio | ✅ | — |
publish_video | Allow publishing video | ✅ | — |
publish_data | Allow sending data channel messages | ✅ | — |
subscribe | Allow subscribing to remote audio and video | ✅ | ✅ |
GET /v1/meta/privileges returns this table (per role). Client SDKs and documentation read the same data, so you never get "the docs say host can publish while the request is rejected in practice".
Why exp and pexp are separate
Agora AccessToken2 separates "token validity" from "privilege validity", and we keep the same split:
exp: whether the token string itself is still accepted (this determines the renewal window);pexp: whether the privileges inside it still work (usually shorter, for "renew first, tighten privileges later").
The three typical expiry states are therefore distinct:
| State | Condition | SDK event | What the client should do |
|---|---|---|---|
| Privileges about to expire | now > pexp - 30s | token-privilege-will-expire | Renew with your server, then call renewToken(newToken) |
| Privileges expired but the token is still valid | now > pexp and now ≤ exp | token-privilege-did-expire | Renew immediately; publishing is rejected in the meantime |
| Token expired | now > exp | Connection dropped (error -109) | Join the channel again |
The server also raises a callback with event code 203 (token_expiring) before privileges expire, so renewal logic can be triggered server-side; see the Callback events reference.
Wildcard tokens
A token with ch = "*" can enter any channel, which is useful for local integration and for "one client trying several rooms". It is not recommended in production:
- It widens the authorization scope from "one channel" to "the whole project";
- Once leaked, the blast radius is every channel rather than a single one;
- During auditing you cannot infer from the token which channel the user was supposed to be in.
A wildcard token can only be issued by explicitly requesting wildcard: true, and GET /v1/tokens/verify marks it explicitly as wildcard: true in the response.
Validation order and rejection reasons
verifyToken always decides in a fixed order; when troubleshooting, rule out causes in that order:
| Order | Rejection reason (TokenRejection) | Corresponding REST error code |
|---|---|---|
| 1 | malformed / bad_prefix | invalid_token |
| 2 | bad_version | invalid_token |
| 3 | invalid_signature | invalid_token |
| 4 | expired | token_expired |
| 5 | not_yet_valid (iat ahead, 5 seconds of clock skew tolerated by default) | token_not_yet_valid |
| 6 | channel_mismatch | token_channel_mismatch |
| 7 | uid_mismatch | token_uid_mismatch |
| 8 | privilege_expired / missing_privilege | token_privilege_expired |
Signature validation happens before any business validation: when the signature is wrong you are not told whether the channel name matches, so the endpoint cannot be abused as a probe.
uid and userId mapping
A token only contains the numeric uid. IM user ids (strings) are mapped through POST /v1/uids:resolve, and the mapping is created by passing userId when the token is issued. The two parts are independent paths: when a token is valid but the mapping is missing, /v1/uids:resolve returns partial results (resolved / unresolved) instead of failing the whole batch.
Security practices (token-related)
- Keep
appCertificateserver-side only, in key management; never in images, front-end bundles or logs. - Token TTL follows the project's
tokenTtlSecondsby default; for business traffic we recommend staying within one hour, and using renewal rather than a long TTL for long sessions. - Do not use
saltor the payload as business state storage — they change with every issuance. - Rotating
appCertificateinvalidates all issued tokens immediately, so do it during a window in which clients have a renewal path. - Minimise privileges: issue
audiencetokens to viewers only; never ship host privileges to the front end and then "restrict them in the UI".
Related pages
- Issue, renew and verify endpoints: Token authentication
- What changes on the token side when migrating from Agora: Migration guide
- Roles and live scenarios: Roles and live streaming