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

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](https://app.kinescope.io) in **Settings → API tokens**. See more about authorization in the [general API guidelines](https://docs.kinescope.com/developer-guides/api-general-rules/).

```bash
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:

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

```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):

```bash
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:**

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

```bash
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:**

```bash
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:**

```bash
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.

### **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:

```go
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:**
```json
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key-2024-12-25"
}
```

**Payload:**
```json
{
  "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:
   ```bash
   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:

```go
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](https://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](https://docs.kinescope.com/live-streams/live-stream-guide/)** — setting up streams in Kinescope
2. **[General API guidelines](https://docs.kinescope.com/developer-guides/api-general-rules/)** — authorization and request format
3. **[Authorization backend](https://docs.kinescope.com/developer-guides/authorization-backend/)** — video access control

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

