Skip navigation

JWT Authentication for Stream Chat

Updated: 28.04.2026
Open as Markdown

JWT authentication lets you automatically authorize users in the stream chat and control access from your server. This is especially useful if you want to integrate the chat with your user management system.

Who this article is for

  • Platform developers — need to integrate stream chat with an authentication system
  • System administrators — need to control access to stream chat
  • Backend developers — need to set up secure user authorization

When you need JWT chat authentication

Use JWT authentication if:

  • Access control is needed — you want to decide on your server who can write in chat
  • Integration with your system — you need to link chat users with your authentication system
  • Automatic authorization — users should be authorized without additional steps
  • Account system binding — you need to track which of your users is writing in chat

How JWT works in chat (5 steps)

JWT authentication uses asymmetric cryptography (RSA) for secure transmission of user data:

  1. You create a key pair (private and public) on your server
  2. The public key is saved in Kinescope via API (the private key stays only with you)
  3. Your server creates a JWT token with user data and signs it with the private key
  4. The token is passed in the chat URL as a token parameter
  5. Kinescope verifies the signature with the public key and authorizes the user
Advantage of the asymmetric scheme: The private key never leaves your server. Kinescope only receives the public key and can verify signatures, but cannot create fake tokens. This is more secure than a symmetric scheme (HS256), where a shared secret would need to be transmitted to Kinescope.

How to enable JWT authentication

Authentication is configured individually for each stream via the Kinescope API. Pass the chat_jwt_required: true parameter to enable it:

Important: The API token in the Authorization: Bearer header must be in UUID format. You can get a token from the Kinescope Dashboard in Settings → API tokens. See more about authorization in the general API guidelines .

curl --location --request PUT 'https://api.kinescope.io/v2/live/events/{{event_id}}' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer your_api_token_here' \
--data '{
	"chat_jwt_required": true
}'

After enabling, users will be automatically authorized when following a link with a token:

https://kinescope.io/chat/{{event_id}}?token={{jwt}}

Now let’s go through how to set everything up from scratch.

Setup: step 1 — generating keys

JWT authentication uses asymmetric RSA cryptography, which requires a key pair:

  • Private key — stays on your server, used to sign JWT tokens
  • Public key — transmitted to Kinescope via API, used to verify token signatures

What is JWK?

JWK (JSON Web Key) is a standardized format for representing cryptographic keys (RFC 7517). It allows secure key exchange between systems:

  1. Security — the private key never leaves your server
  2. Key rotation — easily replace keys by uploading a new public key
  3. Access revocation — if a key is compromised, immediately delete the public key from Kinescope
  4. Standardization — JWK is supported by most libraries and systems
Why not a symmetric key (HS256)? With a symmetric scheme, you would need to transmit the secret key to Kinescope. This creates a compromise risk: if the key is compromised at Kinescope, an attacker could create fake tokens. With an asymmetric scheme (RS256), even if the public key is compromised, an attacker cannot create tokens since the private key, which stays only with you, is needed for that.

JWK generation example

Here’s what key pair generation looks like:

package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "encoding/base64"
    "encoding/json"
    "time"
    
    "github.com/go-jose/go-jose/v3"
)

type JWK struct {
    Kty string `json:"kty"` // key type (RSA)
    Kid string `json:"kid"` // key identifier (unique)
    Use string `json:"use"` // purpose (sig for signing)
    Alg string `json:"alg"` // algorithm (RS256)
    N   string `json:"n"`   // RSA key modulus (base64url-encoded)
    E   string `json:"e"`   // exponent (usually "AQAB")
}

// Generate 2048-bit RSA key pair
func generateRSAKeyPair() (*rsa.PrivateKey, *JWK, error) {
    privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil {
        return nil, nil, err
    }
    
    // Generate unique Key ID (kid)
    kid := "key-" + time.Now().Format("2006-01-02")
    
    // Build public key in JWK format
    publicKeyJWK := &JWK{
        Kty: "RSA",
        Kid: kid,
        Use: "sig",   // purpose - signing
        Alg: "RS256", // algorithm - RSA with SHA-256
        N:   base64.RawURLEncoding.EncodeToString(privateKey.PublicKey.N.Bytes()),
        E:   base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}), // 65537 = AQAB
    }
    
    return privateKey, publicKeyJWK, nil
}

// Save private key in PEM format
func savePrivateKey(key *rsa.PrivateKey) ([]byte, error) {
    return x509.MarshalPKCS8PrivateKey(key)
}

Example of a ready public JWK in JSON:

{
  "kty": "RSA",
  "kid": "key-2024-12-25",
  "use": "sig",
  "alg": "RS256",
  "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbwE...",
  "e": "AQAB"
}

Field descriptions:

  • kty — key type (RSA)
  • kid — key identifier (unique in your system)
  • use — key purpose (sig for signing)
  • alg — signing algorithm (RS256 for RSA with SHA-256)
  • n — RSA key modulus (base64url-encoded)
  • e — RSA key exponent (usually AQAB, which means 65537)

Saving the public key in Kinescope

After generating the JWK, save the public part of the key in Kinescope via API. Make sure to pass all parameters: kty, e, use, kid, alg, n, and expires_at (key expiry date in ISO 8601 format):

curl --location 'https://api.kinescope.io/v1/jwk' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer your_api_token_here' \
--data '{
	"kty": "RSA",
	"e": "AQAB",
	"use": "sig",
	"kid": "key-2024-12-25",
	"alg": "RS256",
	"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbwE...",
	"expires_at": "2026-12-31T23:59:59Z"
}'

Example successful response:

{
  "id": "jwk_abc123def456",
  "kty": "RSA",
  "kid": "key-2024-12-25",
  "created_at": "2024-12-25T10:00:00Z",
  "expires_at": "2026-12-31T23:59:59Z"
}

Key management

View all active keys:

curl --location 'https://api.kinescope.io/v1/jwk' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer your_api_token_here'

Get data for a specific key:

curl --location 'https://api.kinescope.io/v1/jwk/{{jwk_id}}' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer your_api_token_here'

Delete a key:

curl --location --request DELETE 'https://api.kinescope.io/v1/jwk/{{jwk_id}}' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer your_api_token_here'

Setup: step 2 — generating a JWT token

Create a JWT token (JSON Web Token, RFC 7519) with required fields and sign it with your private key.

Required fields

  • aud (audience) — value "chat" (required). Kinescope verifies that the token is intended for chat.
  • user_id — unique user identifier in your system. Used for identification in chat.
  • username — username for display in chat. Will be shown next to messages.
  • event_id — stream ID (required). Can be found in your dashboard or in the stream link.

Important: event_id is required for chat embedding — chat can be embedded on a page without JWT authentication, but when using JWT, event_id is required to bind the token to a specific stream.

  • exp (expiration) — token expiry time (Unix timestamp). Recommended to set a short lifespan (e.g., 1 hour).
  • nbf (not before) — time before which the token is invalid (Unix timestamp). Useful for tokens that should become active in the future.
  • iat (issued at) — token creation time (Unix timestamp). Helps track token age.

All standard JWT fields will be checked by the Kinescope system when validating the token.

JWT generation example

Here’s what JWT generation and signing looks like:

package main

import (
    "crypto/rsa"
    "time"
    
    "github.com/golang-jwt/jwt/v5"
)

type ChatClaims struct {
    UserID   string `json:"user_id"`
    Username string `json:"username"`
    EventID  string `json:"event_id"`
    jwt.RegisteredClaims
}

// Generate JWT token
func generateJWT(privateKey *rsa.PrivateKey, kid string, userID, username, eventID string) (string, error) {
    now := time.Now()
    
    claims := ChatClaims{
        UserID:   userID,
        Username: username,
        EventID:  eventID,
        RegisteredClaims: jwt.RegisteredClaims{
            Audience:  []string{"chat"}, // must be "chat"
            IssuedAt:  jwt.NewNumericDate(now),
            ExpiresAt: jwt.NewNumericDate(now.Add(1 * time.Hour)), // default 1 hour
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
    token.Header["kid"] = kid // Key ID
    
    return token.SignedString(privateKey)
}

// Usage example
func main() {
    privateKey, publicKeyJWK, err := generateRSAKeyPair()
    if err != nil {
        panic(err)
    }
    
    kid := publicKeyJWK.Kid
    
    jwtToken, err := generateJWT(
        privateKey,
        kid,
        "user-12345",    // User ID
        "John Smith",    // Username
        "event-abc-123", // Stream ID
    )
    if err != nil {
        panic(err)
    }
    
    println("JWT token:", jwtToken)
}

For developers: In a real implementation, use libraries for JWT generation:

  • Node.js: node-jsonwebtoken (jwt.sign()), jose (new SignJWT().setProtectedHeader().sign())
  • Browser: Web Crypto API (crypto.subtle.sign())
  • Python: PyJWT (jwt.encode())
  • Go: github.com/golang-jwt/jwt/v5 (jwt.NewWithClaims().SignedString())

JWT token structure

A JWT consists of three parts separated by dots: header.payload.signature

Header:

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key-2024-12-25"
}

Payload:

{
  "aud": "chat",
  "user_id": "user-12345",
  "username": "John Smith",
  "event_id": "event-abc-123",
  "iat": 1703500800,
  "exp": 1703504400
}

Signature: The signature is created by hashing base64(header) + "." + base64(payload) using the private key with the RS256 algorithm.

Using the token

After generating the token, pass it in the chat URL:

https://kinescope.io/chat/event-abc-123?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0yMDI0LTEyLTI1In0...

Done! Your users can now be automatically authorized in the stream chat via JWT tokens.

Security

Key rotation

It is recommended to regularly update keys to improve security. The rotation process:

  1. Generate a new key pair (private and public)
  2. Upload the new public key to Kinescope via API (the old key will remain active)
  3. Start using the new private key to sign new tokens
  4. After the overlap period (when all old tokens expire), delete the old public key from Kinescope
Overlap period: It is recommended to keep the old key active for a period equal to the maximum token lifespan (e.g., if tokens live 24 hours, keep the old key for 24 hours after starting to use the new one).

Actions when a key is compromised

If the private key was compromised (leak, suspected breach):

  1. Immediately delete the public key from Kinescope via API:
    curl --location --request DELETE 'https://api.kinescope.io/v1/jwk/{{jwk_id}}' \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer your_api_token_here'
    
  2. Generate a new key pair and upload the new public key
  3. Notify users that they need to re-authorize (if required)
  4. Check logs for suspicious activity
Important: After deleting the key, all tokens signed with that key will become invalid. Users will need to obtain new tokens.

Security recommendations

  • Key size: Use keys of at least 2048 bits (recommended for RSA)
  • Token lifespan: Set a short token lifespan (1–24 hours). For long-lived sessions, use a token refresh mechanism
  • Key lifespan: Set expires_at for public keys (e.g., 1–2 years) and plan rotation before expiry
  • Private key storage: Store the private key in a secure vault (e.g., Kubernetes secrets, AWS Secrets Manager, or an encrypted store)
  • Monitoring: Track key usage and suspicious activity
  • Algorithm: Use only RS256 (RSA with SHA-256). Other algorithms are not supported

Limitations

  • Supported algorithms: Only RS256 (RSA with SHA-256)
  • Minimum key size: 2048 bits
  • Maximum number of active keys: A limit may apply at the account level (check with support)

Troubleshooting

Token not accepted by the system

Problem: The user cannot authorize, the token is rejected.

Possible causes and solutions:

  1. Invalid token signature

    • Make sure you are using the correct private key for signing
    • Verify that the public key is uploaded to Kinescope and is active
    • Make sure you are using the RS256 algorithm
  2. Expired key

    • Check the expires_at field of the public key in Kinescope
    • If the key has expired, upload a new public key
  3. Incorrect event_id

    • Make sure the event_id in the token matches the stream ID
    • Verify that the stream exists and JWT authentication is enabled for it
    • Important: event_id is required for chat embedding — chat can be embedded without JWT, but when using JWT, a valid event_id is required
  4. Expired token

    • Check the exp field in the token (expiry time)
    • Make sure the system clock on the server is synchronized (NTP)
    • Generate new tokens with an up-to-date expiry time
  5. Incorrect aud field

    • Make sure the aud field has the value "chat" (strictly lowercase)

How to verify token validity

You can verify the token locally before sending it to the user. Here is an example verification function:

package main

import (
    "crypto/rsa"
    "errors"
    
    "github.com/golang-jwt/jwt/v5"
)

// Verify JWT token validity
func verifyJWT(tokenString string, publicKey *rsa.PublicKey) (*ChatClaims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &ChatClaims{}, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, errors.New("unsupported algorithm")
        }
        return publicKey, nil
    })
    
    if err != nil {
        return nil, err
    }
    
    if claims, ok := token.Claims.(*ChatClaims); ok && token.Valid {
        if !claims.VerifyAudience("chat", true) {
            return nil, errors.New("invalid audience")
        }
        return claims, nil
    }
    
    return nil, errors.New("invalid token")
}

For developers: In a real implementation, use libraries for JWT verification:

  • Node.js: node-jsonwebtoken (jwt.verify()), jose (jwtVerify())
  • Browser: Web Crypto API (crypto.subtle.verify())
  • Python: PyJWT (jwt.decode())
  • Go: github.com/golang-jwt/jwt/v5 (jwt.ParseWithClaims())

Online tools:

  • jwt.io — decoding and checking token structure (without signature verification)
  • Check payload structure: all required fields should be present

Common errors when generating keys

  1. Error “invalid key format”

    • Make sure you are using the correct JWK format (RFC 7517)
    • Check that all required fields are present: kty, e, n, kid, alg, use
  2. Error “key size too small”

    • Use keys of at least 2048 bits
    • When generating: rsa.GenerateKey(rand.Reader, 2048)
  3. Error “key expired”

    • Check the expires_at field when uploading the key
    • Make sure the expiry date is in ISO 8601 format: "2026-12-31T23:59:59Z"

Chat embed issues

Problem: Chat is not displayed or does not authorize the user when embedded.

Solutions:

  1. Check event_id in the token — it must match the stream ID to which the chat is linked
  2. Make sure JWT authentication is enabled for the stream via API
  3. Check the URL format — the token must be passed as a token parameter:
    https://kinescope.io/chat/{{event_id}}?token={{jwt_token}}
    
    Example:
    https://kinescope.io/chat/event-abc-123?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
    
  4. Check the browser console for JavaScript errors

If the problem is not resolved, contact the support chat within the Kinescope interface with:

  • Stream ID (event_id)
  • Token example (sensitive data can be masked)
  • Problem description and reproduction steps

Done! You can now set up JWT authentication for stream chat and automatically authorize users.

What’s next?

  1. Live stream guide — setting up streams in Kinescope
  2. General API guidelines — authorization and request format
  3. Authorization backend — video access control

Still have questions? Write to the support chat within the Kinescope interface — our specialists will help!