JWT authentication for Web SDK

This guide explains how to enable JWT authentication in your Bloomreach Web SDK integration to enhance event stream tracking security with cryptographically signed tokens.

Overview

JWT authentication adds an optional security layer to your Bloomreach Web SDK implementation. When enabled, the SDK automatically attaches a Bearer token to tracking requests sent to secured endpoints.

Key capabilities:

  • Automatic authentication: SDK attaches JWT tokens to secured tracking requests

  • Automatic token refresh: When a request returns HTTP 401, the SDK calls your refresh callback, waits for a new token, and retries the request

  • Backward compatible: JWT support is completely optional, so existing integrations continue working without changes

JWT authentication, in combination with Event stream permissions, ensures that sensitive tracking data is accessed or updated only by authenticated users. When you configure customer IDs, events, or properties as signed-only in your Event stream, the SDK provides the required JWT to authorize those operations.

🚧

Warning

The Web SDK relies on the cookie identifier to track all visitors, including anonymous ones. If you set the cookie to Signed-only or Deny in your event stream's customer ID permissions, the SDK will stop working for any visitor who doesn't have a valid JWT, including all anonymous traffic.

Data hub warns you before saving this configuration, but doesn't prevent it. If your SDK stops tracking after a permissions change, check that cookie is set to Allow in your event stream's customer ID permissions.

Prerequisites

Before implementing JWT authentication, ensure you have:

  • Event stream with JWT enabled: Your Event stream must be configured with JWT signing keys and signed-only permissions. See JWT validation in Event stream security and permissions for configuration details.

  • Backend JWT generation: Your backend must be able to generate and sign JWT tokens using one of your JWT signing keys.

  • Understanding the authentication flow: You should know when users authenticate in your website or application and how to retrieve fresh tokens from your backend.

Configure JWT authentication

JWT authentication is configured through an auth object in your Web SDK snippet. Add this object to your snippet configuration alongside target and stream_id.

Auth object structure

The auth object requires two fields:

auth: {
  token: string,           // Initial JWT available at page load
  update_jwt_token: () => Promise<string>  // Callback to retrieve fresh JWT
}
🚧

Important

If you include the auth object, both token and update_jwt_token are required. Omitting auth entirely disables JWT mode.

Installation snippet example

<script>
  // Your function to fetch JWT from backend
  async function getJwtFromBackend() {
    const response = await fetch('https://your-backend.com/api/get-jwt', {
      credentials: 'include'  // Include session cookies
    });
    const data = await response.json();
    return data.token;
  }
</script>

<script>
!function(e,t,n,i,o,r){
  // ... standard brweb snippet bootstrap ...
}(document,"brweb","script","webxpClient",window,{
  target: "https://api.exponea.com",
  stream_id: "40494d18-e714-4c8c-bbd9-35225de4e81d",
  auth: {
    // Initial JWT token available when page loads
    token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
    
    // Callback SDK uses to get fresh token when needed
    update_jwt_token: async () => {
      return getJwtFromBackend();
    }
  },
  // ... other configuration options
});

brweb.start();
</script>

In this configuration:

  • The SDK uses token immediately for secured requests when the page loads

  • When a secured request returns 401 (expired token), the SDK automatically calls update_jwt_token

  • The SDK waits for the new token, updates its internal state, and retries the failed request

Generate JWT tokens on your backend

Your backend must issue a JWT that the event stream endpoint can validate. The SDK sends the token in the Authorization: Bearer <token> header on every secured request.

Token specification

Header:

FieldRequiredValueNotes
algYesHS256, HS384, or HS512Other algorithms are rejected.
kidYesKey ID from Data hubIdentifies the signing key. Must be non-empty.

Payload:

FieldRequiredDescription
idsYesMap of customer identifiers to sign. For example {"registered": "user123"}. Keys and values must be non-empty strings. Must match the identifiers sent in the tracking request.
expYesToken expiry as a Unix timestamp. Must not be in the past. Match your session duration and don't exceed 90 days.
iatNoParsed by the endpoint but not validated.

Code examples

const jwt = require('jsonwebtoken');
function generateBloomreachJWT(userId) {
  const SECRET = process.env.BLOOMREACH_JWT_SECRET;
  const KEY_ID = process.env.BLOOMREACH_JWT_KEY_ID;

  return jwt.sign(
    { ids: { registered: userId } },
    SECRET,
    { algorithm: 'HS256', expiresIn: '24h', keyid: KEY_ID }
  );
}
import jwt, os, datetime
def generate_bloomreach_jwt(user_id):
    secret = os.environ['BLOOMREACH_JWT_SECRET']
    key_id = os.environ['BLOOMREACH_JWT_KEY_ID']

    return jwt.encode(
        {
            "ids": {"registered": user_id},
            "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24)
        },
        secret,
        algorithm="HS256",
        headers={"kid": key_id},
    )

Things to keep in mind

  • kid identifies, secret signs. When you create a signing key in Data hub, you receive both values. The kid goes in the JWT header; the secret is used to compute the HMAC signature. They are separate values.

  • ids values must match exactly. If the JWT signs {"registered": "user123"}, the tracking request must send the same registered: user123. A mismatch rejects the entire request.

  • Secure your token endpoint. Only authenticated users with a valid backend session should be able to request a JWT containing their customer ID.

Implement token refresh callback

Your update_jwt_token callback is responsible for obtaining a fresh JWT from your backend and returning it to the SDK. The SDK calls this function automatically when it receives a 401 response from a secured endpoint.

Callback contract

Return type: Promise<string>

On success:

  • Return a non-empty string or a promise of a non-empty string containing a valid JWT

  • The JWT should be signed with one of your JWT signing keys

  • The token should have an appropriate expiration matching your session duration

On failure:

  • Reject the promise or throw an Error

  • Any falsy return value is treated as an error by the SDK

Implementation example

async function getJwtFromBackend() {
  try {
    const response = await fetch('https://your-backend.com/api/jwt', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json'
      }
    });
    
    if (!response.ok) {
      throw new Error(`JWT fetch failed: ${response.status}`);
    }
    
    const data = await response.json();
    
    if (!data.token) {
      throw new Error('No token in response');
    }
    
    return data.token;
  } catch (error) {
    console.error('Failed to refresh JWT:', error);
    throw error;  // Re-throw to signal failure to SDK
  }
}

SDK behavior on refresh failure

When your update_jwt_token callback fails (promise rejected or error thrown):

  1. The SDK clears its internally stored JWT token

  2. Subsequent secured requests receive HTTP 403 from the backend (if Signed-only permissions are configured)

  3. Tracking continues for unsecured endpoints (experiments, web layers)

  4. Secured tracking (events with signed-only customer IDs, properties, or event types) stops until a new valid token is provided

The SDK does not verify JWT signatures, issuers, or claims. All JWT verification occurs on the backend during request processing.

JWT behavior in the SDK

The SDK manages JWT tokens automatically to ensure secure, authenticated tracking without requiring manual intervention for each request.

Automatic token refresh

When a secured request returns HTTP 401 Unauthorized, the SDK performs the following sequence:

  1. Call refresh callback: Invokes your update_jwt_token function

  2. Wait for new token: Blocks secured commands until refresh completes

  3. Update internal state: Stores the new token in memory

  4. Retry failed request: Automatically retries the original request with the new token

  5. Resume normal operation: Continues processing queued commands

During token refresh:

  • Secured commands (tracking with signed-only requirements) are blocked and wait for the new token

  • Unsecured commands (experiments, web layer requests) continue processing normally

  • If multiple requests trigger 401 simultaneously, the SDK reuses the same refresh promise instead of calling your callback multiple times

JWT and anonymization

When you call brweb.anonymize() and a JWT is currently active, the SDK adjusts its behavior to prevent mixing data between different user identities:

  1. Flush queue: The SDK sends all queued tracking commands in a best-effort attempt (no retries)

  2. Clear JWT token: Removes the JWT from memory to disassociate from the previous user identity

  3. Generate new identity: Creates a new browser cookie for the anonymous user

  4. Continue tracking: Subsequent tracking uses the new anonymous identity without JWT

// User logs out - switch to anonymous tracking
brweb.anonymize();
// SDK automatically clears JWT and starts fresh anonymous session

This sequence ensures that tracking data belonging to different authenticated users never mixes in the same tracking queue.

Troubleshooting

Token refresh callback never resolves

Problem: The SDK stops tracking to secured endpoints but continues working for experiments.

Cause: Your update_jwt_token callback is hanging or taking too long to resolve.

Solution: Add timeout handling to your backend fetch:

async function getJwtFromBackend() {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 5000);
  
  try {
    const response = await fetch('https://your-backend.com/api/jwt', {
      signal: controller.signal,
      credentials: 'include'
    });
    clearTimeout(timeoutId);
    
    const data = await response.json();
    return data.token;
  } catch (error) {
    clearTimeout(timeoutId);
    throw error;
  }
}

Receiving 403 errors after implementing JWT

Problem: All secured tracking requests return HTTP 403 Forbidden.

Cause: The JWT token is invalid, expired, or not signed with the correct signing key.

Solution:

  1. Verify your backend is using the correct signing key from the Event streams settings

  2. Check that the JWT includes the required customer IDs in the payload

  3. Ensure the JWT expiration (exp claim) is set appropriately

  4. Validate the JWT structure matches your Event stream's signed-only configuration

Tracking stops after user authentication

Problem: After users log in, no tracking events appear in Bloomreach.

Cause: The token field in the initial auth configuration contains an invalid or expired token.

Solution: Either provide a valid initial token or use an empty string and rely on the first 401 to trigger the refresh callback:

auth: {
  token: '',  // Empty initial token
  update_jwt_token: async () => {
  return getJwtFromBackend();
  }
}

Next steps

After implementing JWT authentication in your Web SDK:

  1. Configure Event stream security and permissions to define which customer IDs, events, and properties require JWT

  2. Review Web SDK usage and limitations for additional security considerations

  3. Test your implementation using the Bloomreach Tracking Console extension to verify JWT tokens are being sent correctly

  4. Monitor your Event stream for rejected requests due to invalid or missing JWT tokens


© Bloomreach, Inc. All rights reserved.