Skip to content

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 target peer_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:

ItemNotes
access_idThe application credential ID assigned by the TiRTC platform
secret_keyThe application secret paired with access_id
peer_idThe target to connect to, for example device://dev_xxx
device_secret_keyThe device secret associated with the target device
subThe 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:

text
<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 request
  • scope: the authorized target scope, such as connect:device://dev_xxx
  • iss: the current application's access_id
  • iat: the issued-at time
  • exp: the expiration time
  • nonce: 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:

text
v1.<payload_b64>.<app_sig>

Where:

  • v1 is the version
  • payload_b64 is the base64url-encoded payload JSON
  • app_sig is the final signature

Build the token in these five steps:

  1. Build the payload JSON with sub, scope, iss, iat, exp, and nonce
  2. Base64url-encode the payload JSON to get payload_b64
  3. Use device_secret_key to calculate an HMAC-SHA256 over payload_b64, producing device_sig
  4. Use secret_key to calculate an HMAC-SHA256 over payload_b64 + "." + device_sig, producing the final app_sig
  5. Concatenate them as v1.<payload_b64>.<app_sig>

In pseudocode:

text
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_sig

Full Example and Go Implementation

Start with a concrete payload example:

json
{
  "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:

go
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:

  • scope must bind to the target peer_id
  • sub should contain a stable subject identifier chosen by you
  • ttlSeconds should stay short
  • nonce must be regenerated every time
  • device_secret_key signs first, then secret_key signs the final string
  • The token returned to the client includes only app_sig; device_sig is not returned separately

Security Requirements

These are the most common production mistakes:

  • Do not send secret_key to the client
  • Do not send device_secret_key to the client
  • Return only a short-lived connection token to the client
  • Use a new nonce every time you generate a connection token
  • If you need user-level or device-level access control, complete that decision before issuing the token

TiRTC