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.
WarningThe Web SDK relies on the
cookieidentifier to track all visitors, including anonymous ones. If you set thecookieto 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
cookieis 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
}
ImportantIf you include the
authobject, bothtokenandupdate_jwt_tokenare required. Omittingauthentirely 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
tokenimmediately 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:
| Field | Required | Value | Notes |
|---|---|---|---|
alg | Yes | HS256, HS384, or HS512 | Other algorithms are rejected. |
kid | Yes | Key ID from Data hub | Identifies the signing key. Must be non-empty. |
Payload:
| Field | Required | Description |
|---|---|---|
ids | Yes | Map 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. |
exp | Yes | Token expiry as a Unix timestamp. Must not be in the past. Match your session duration and don't exceed 90 days. |
iat | No | Parsed 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
-
kididentifies,secretsigns. When you create a signing key in Data hub, you receive both values. Thekidgoes in the JWT header; the secret is used to compute the HMAC signature. They are separate values. -
idsvalues must match exactly. If the JWT signs{"registered": "user123"}, the tracking request must send the sameregistered: 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):
-
The SDK clears its internally stored JWT token
-
Subsequent secured requests receive HTTP 403 from the backend (if Signed-only permissions are configured)
-
Tracking continues for unsecured endpoints (experiments, web layers)
-
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:
-
Call refresh callback: Invokes your
update_jwt_tokenfunction -
Wait for new token: Blocks secured commands until refresh completes
-
Update internal state: Stores the new token in memory
-
Retry failed request: Automatically retries the original request with the new token
-
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:
-
Flush queue: The SDK sends all queued tracking commands in a best-effort attempt (no retries)
-
Clear JWT token: Removes the JWT from memory to disassociate from the previous user identity
-
Generate new identity: Creates a new browser cookie for the anonymous user
-
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 sessionThis 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:
-
Verify your backend is using the correct signing key from the Event streams settings
-
Check that the JWT includes the required customer IDs in the payload
-
Ensure the JWT expiration (
expclaim) is set appropriately -
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:
-
Configure Event stream security and permissions to define which customer IDs, events, and properties require JWT
-
Review Web SDK usage and limitations for additional security considerations
-
Test your implementation using the Bloomreach Tracking Console extension to verify JWT tokens are being sent correctly
-
Monitor your Event stream for rejected requests due to invalid or missing JWT tokens
Updated about 4 hours ago
