nimeet Docs
中文

AccessToken2 Semantics and Privileges

Privilege bits, channel/uid binding, exp vs pexp, wildcard tokens, renewal grace and differences from Agora.

Version v1.1.0 · Updated 2026-10-26 · revision 1

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

text
nrt1.<base64url(payload JSON)>.<base64url(HMAC-SHA256(appCertificate, "nrt1.<base64url(payload)>"))>
SegmentContentDescription
Prefixnrt1Format version; if the structure changes we will bump it to nrt2 and keep the SDKs and the gateway compatible with both for a while
payloadbase64url(JSON)Fields in the table below; it is not encrypted, anyone can decode it, so do not put sensitive information in it
Signaturebase64url(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:

FieldTypeMeaning
vnumberVersion, currently 1
appstringProject App ID (32 hex characters)
chstringChannel name; * means wildcard (any channel)
uidnumberNumeric uid, 1..2147483647
rolehost \audienceDetermines the default privilege set
privstring[]The effective privilege bits (see the table below); the only basis for authorization decisions
iatnumberIssued at (epoch seconds)
expnumberToken lifetime expiry (epoch seconds)
pexpnumberPrivilege expiry (epoch seconds), always ≤ exp
saltnumberRandom number that makes identical inputs produce different tokens (prevents fingerprinting and replay clustering)
usrstring?Identity hint used only for auditing and log correlation, not part of authorization

Privilege bits

Privilege bitMeaninghostaudience
join_channelAllow joining the channel✅✅
publish_audioAllow publishing audio✅—
publish_videoAllow publishing video✅—
publish_dataAllow sending data channel messages✅—
subscribeAllow 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:

StateConditionSDK eventWhat the client should do
Privileges about to expirenow > pexp - 30stoken-privilege-will-expireRenew with your server, then call renewToken(newToken)
Privileges expired but the token is still validnow > pexp and now ≤ exptoken-privilege-did-expireRenew immediately; publishing is rejected in the meantime
Token expirednow > expConnection 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:

OrderRejection reason (TokenRejection)Corresponding REST error code
1malformed / bad_prefixinvalid_token
2bad_versioninvalid_token
3invalid_signatureinvalid_token
4expiredtoken_expired
5not_yet_valid (iat ahead, 5 seconds of clock skew tolerated by default)token_not_yet_valid
6channel_mismatchtoken_channel_mismatch
7uid_mismatchtoken_uid_mismatch
8privilege_expired / missing_privilegetoken_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.

  1. Keep appCertificate server-side only, in key management; never in images, front-end bundles or logs.
  2. Token TTL follows the project's tokenTtlSeconds by default; for business traffic we recommend staying within one hour, and using renewal rather than a long TTL for long sessions.
  3. Do not use salt or the payload as business state storage — they change with every issuance.
  4. Rotating appCertificate invalidates all issued tokens immediately, so do it during a window in which clients have a renewal path.
  5. Minimise privileges: issue audience tokens to viewers only; never ship host privileges to the front end and then "restrict them in the UI".

Generated by the documentation build pipeline · 2026-10-31 01:24:02 UTC · Documentation center v1.1.0.

Content follows the public API definitions.

ni-meet is a self-hosted RTC PaaS with a LiveKit-based media plane (Apache-2.0) and an Agora-compatible API.