Generate a Connection Token
This guide explains how to generate a token that authorizes a connection to a specific peer_id.
Where the Token Fits in the Full Flow
A connection token is a short-lived credential used to verify that a client is allowed to connect to the target peer_id.
A typical end-to-end flow looks like this:
- The client sends an HTTP request to your business backend asking for a token
- Your backend issues the token
- Before issuing it, your backend can decide whether access is allowed based on the client user identity, the target
device_id, or the targetpeer_id - Only after that check passes should the backend generate the token for this connection
- The backend then returns the token in the HTTP response
This guide focuses on the middle step only: the inputs required to generate the connection token, the claims that must be included, and the exact calculation rule.
In production, the most common approach is to sign the token in your own backend service. TiRTC does not require that this be a traditional backend program. Any environment you control is fine as long as the signing secrets never reach the client.
Required Inputs Before You Generate the Token
Prepare the following inputs:
| Item | Notes |
|---|---|
access_id | The application credential ID assigned by the TiRTC platform |
secret_key | The application secret paired with access_id |
peer_id | The target to connect to, for example device://dev_xxx |
device_secret_key | The device secret associated with the target device |
sub | The subject of the request, such as your user_id, uid, or another stable primary identifier |
If what you have is a device license, the Nano format is:
<device_id>,<device_secret_key>When generating the connection token, the actual input used in signing is the second part, device_secret_key.
If the target of this connection is a device, you can set peer_id directly to device://<device_id>.
Required Claims and How the Token Is Calculated
The token payload must express at least the following information:
sub: the subject identity of this requestscope: the authorized target scope, such asconnect:device://dev_xxxiss: the current application'saccess_idiat: the issued-at timeexp: the expiration timenonce: a per-issuance random value used for replay protection
This token should be short-lived, for example 300 seconds.
The final string returned to the client uses this format:
v1.<payload_b64>.<app_sig>Where:
v1is the versionpayload_b64is the base64url-encoded payload JSONapp_sigis the final signature
Build the token in these five steps:
- Build the payload JSON with
sub,scope,iss,iat,exp, andnonce - Base64url-encode the payload JSON to get
payload_b64 - Use
device_secret_keyto calculate an HMAC-SHA256 overpayload_b64, producingdevice_sig - Use
secret_keyto calculate an HMAC-SHA256 overpayload_b64 + "." + device_sig, producing the finalapp_sig - Concatenate them as
v1.<payload_b64>.<app_sig>
In pseudocode:
payload_json = {
"sub": <subject>,
"scope": "connect:" + <peer_id>,
"iss": <access_id>,
"iat": <issued_at>,
"exp": <expires_at>,
"nonce": <random_nonce>
}
payload_b64 = base64url(payload_json)
device_sig = HMAC_SHA256(device_secret_key, payload_b64)
app_sig = HMAC_SHA256(secret_key, payload_b64 + "." + device_sig)
token = "v1." + payload_b64 + "." + app_sigFull Example and Go Implementation
Start with a concrete payload example:
{
"sub": "user_123",
"scope": "connect:device://dev_xxx",
"iss": "ak_xxx",
"iat": 1740000000,
"exp": 1740000300,
"nonce": "random_128bit_nonce"
}After you base64url-encode this payload, you get payload_b64. Then follow the double-signing rule from the previous section to get app_sig, and finally concatenate the full token.
The following Go example implements the same rule directly:
package tirtc
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"time"
)
type TokenClaims struct {
Subject string `json:"sub"`
Scope string `json:"scope"`
Issuer string `json:"iss"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
Nonce string `json:"nonce"`
}
func hmacB64(key, content string) string {
h := hmac.New(sha256.New, []byte(key))
_, _ = h.Write([]byte(content))
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}
func randomNonce() (string, error) {
buf := make([]byte, 16)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}
func GenerateConnectToken(accessID, secretKey, peerID, deviceSecretKey, subject string, ttlSeconds int64) (string, error) {
now := time.Now().Unix()
nonce, err := randomNonce()
if err != nil {
return "", err
}
payload := TokenClaims{
Subject: subject,
Scope: "connect:" + peerID,
Issuer: accessID,
IssuedAt: now,
ExpiresAt: now + ttlSeconds,
Nonce: nonce,
}
payloadJSON, err := json.Marshal(payload)
if err != nil {
return "", err
}
payloadB64 := base64.RawURLEncoding.EncodeToString(payloadJSON)
deviceSig := hmacB64(deviceSecretKey, payloadB64)
appSig := hmacB64(secretKey, payloadB64+"."+deviceSig)
return "v1." + payloadB64 + "." + appSig, nil
}The key points in this code are:
scopemust bind to the targetpeer_idsubshould contain a stable subject identifier chosen by youttlSecondsshould stay shortnoncemust be regenerated every timedevice_secret_keysigns first, thensecret_keysigns the final string- The token returned to the client includes only
app_sig;device_sigis not returned separately
Security Requirements
These are the most common production mistakes:
- Do not send
secret_keyto the client - Do not send
device_secret_keyto the client - Return only a short-lived connection token to the client
- Use a new
nonceevery time you generate a connection token - If you need user-level or device-level access control, complete that decision before issuing the token