JWT Authentication for Stream Chat
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:
- You create a key pair (private and public) on your server
- The public key is saved in Kinescope via API (the private key stays only with you)
- Your server creates a JWT token with user data and signs it with the private key
- The token is passed in the chat URL as a
tokenparameter - Kinescope verifies the signature with the public key and authorizes the user
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: Bearerheader 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:
- Security — the private key never leaves your server
- Key rotation — easily replace keys by uploading a new public key
- Access revocation — if a key is compromised, immediately delete the public key from Kinescope
- Standardization — JWK is supported by most libraries and systems
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 (sigfor signing)alg— signing algorithm (RS256for RSA with SHA-256)n— RSA key modulus (base64url-encoded)e— RSA key exponent (usuallyAQAB, 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_idis required for chat embedding — chat can be embedded on a page without JWT authentication, but when using JWT,event_idis required to bind the token to a specific stream.
Standard JWT fields (recommended)
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:
- Generate a new key pair (private and public)
- Upload the new public key to Kinescope via API (the old key will remain active)
- Start using the new private key to sign new tokens
- After the overlap period (when all old tokens expire), delete the old public key from Kinescope
Actions when a key is compromised
If the private key was compromised (leak, suspected breach):
- 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' - Generate a new key pair and upload the new public key
- Notify users that they need to re-authorize (if required)
- Check logs for suspicious activity
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_atfor 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:
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
Expired key
- Check the
expires_atfield of the public key in Kinescope - If the key has expired, upload a new public key
- Check the
Incorrect
event_id- Make sure the
event_idin the token matches the stream ID - Verify that the stream exists and JWT authentication is enabled for it
- Important:
event_idis required for chat embedding — chat can be embedded without JWT, but when using JWT, a validevent_idis required
- Make sure the
Expired token
- Check the
expfield 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
- Check the
Incorrect
audfield- Make sure the
audfield has the value"chat"(strictly lowercase)
- Make sure the
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
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
Error “key size too small”
- Use keys of at least 2048 bits
- When generating:
rsa.GenerateKey(rand.Reader, 2048)
Error “key expired”
- Check the
expires_atfield when uploading the key - Make sure the expiry date is in ISO 8601 format:
"2026-12-31T23:59:59Z"
- Check the
Chat embed issues
Problem: Chat is not displayed or does not authorize the user when embedded.
Solutions:
- Check
event_idin the token — it must match the stream ID to which the chat is linked - Make sure JWT authentication is enabled for the stream via API
- Check the URL format — the token must be passed as a
tokenparameter:
Example:https://kinescope.io/chat/{{event_id}}?token={{jwt_token}}https://kinescope.io/chat/event-abc-123?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... - 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?
- Live stream guide — setting up streams in Kinescope
- General API guidelines — authorization and request format
- Authorization backend — video access control
Still have questions? Write to the support chat within the Kinescope interface — our specialists will help!