Tokens
TokenRequest, TokenDetails and the Blackevin JWT — what your server signs, what Blackevin issues, and what the client sends.
Three things with similar names
| what it is | who makes it | |
|---|---|---|
| TokenRequest | a signed ask for a token | your server, locally |
| TokenDetails | the answer, wrapping the token | Blackevin |
| Blackevin JWT | the credential itself | Blackevin |
The usual flow is: your server signs a TokenRequest, the browser exchanges it
for TokenDetails, and the JWT inside is what connects. The SDK does the last two
steps for you if you give it authUrl — see
authentication.
TokenRequest
{
"keyName": "k1",
"ttl": 3600000,
"capability": "{\"chat:*\":[\"subscribe\",\"publish\"]}",
"clientId": "alice",
"timestamp": 1710000000000,
"nonce": "0123456789abcdef",
"mac": "<base64 HMAC-SHA256>"
}The MAC is computed over these six fields in this order, UTF-8, with a newline after each one including empty ones:
keyName
ttl
capability
clientId
timestamp
nonceTwo things people get wrong, both of which produce the same unhelpful
invalid token request mac:
ttlis milliseconds. An Ably-shaped1.hourin seconds reads as 3.6 seconds here.capabilityis signed as a JSON string, not as an object. Sign the exact bytes you send.
timestamp must be within ±2 minutes of the server's clock, and nonce must be
unique per key and timestamp. Both are replay defences: a captured TokenRequest
is useless two minutes later, and useless twice.
const rest = new Blackevin.Rest({ key: process.env.BLACKEVIN_KEY });
const tokenRequest = await rest.auth.createTokenRequest({
clientId: 'alice',
ttl: 3_600_000,
capability: { 'chat:*': ['subscribe', 'publish'] },
});createTokenRequest needs key, so it only runs somewhere you trust with one.
It signs locally and makes no network call — your login path must not depend on
Blackevin being reachable.
Exchanging it
POST /keys/{keyName}/requestTokenThis endpoint takes no Authorization header: the TokenRequest is itself signed, and that signature is the credential.
An unsigned body is also accepted, with Authorization: Basic base64(key) —
useful from a server that already holds the key and has no reason to HMAC
anything.
TokenDetails
{
"token": "<Blackevin JWT>",
"keyName": "k1",
"issued": 1710000000000,
"expires": 1710003600000,
"capability": "{\"chat:*\":[\"subscribe\",\"publish\"]}",
"clientId": "alice"
}From the SDK:
const details = await realtime.auth.requestToken({ clientId: 'alice' });The Blackevin JWT
HS256, signed with the API key secret, with the key id in the header so the node knows which secret to verify against.
header kid | the keyId |
iat, exp | unix seconds, both required |
x-blackevin-capability | the capability map, as a JSON string. Required |
x-blackevin-client-id | optional. When present it is forced on the connection |
Custom Blackevin names are lowercase kebab-case throughout — claims, AMQP headers,
HTTP extensions: x-blackevin-<words-with-hyphens>.
A token's capabilities must be a subset of the issuing key's. Asking for more is refused at issue time, which names the key rather than the session.
x-blackevin-client-id being forced is what makes presence trustworthy: a client
cannot claim to be someone else, because the identity was decided by whoever
signed the token.
Signing one directly
TokenRequest is the flow for browsers. A server that just wants a scoped token can skip it:
import { signBlackevinJwt } from '@blackevin/core';
const jwt = signBlackevinJwt({
keyName: 'k1',
secret: process.env.BLACKEVIN_SECRET,
capabilities: { 'chat:*': ['subscribe', 'publish', 'presence'] },
clientId: userId,
expiresIn: 3600,
});expiresIn is seconds here, matching exp. The millisecond ttl belongs
to TokenRequest and only to TokenRequest.
Sending it
ws://host/?accessToken=<jwt>Authorization: Bearer <jwt>token is accepted as a query alias for accessToken.
The SDK builds both from whatever you passed — key gives you Basic and the
key query, token gives you Bearer and accessToken.
Refreshing mid-session
A connection can swap its credential without dropping:
{ "op": 15, "id": "1", "accessToken": "…" }The server answers Ack and a fresh Connected. You will not normally write this
yourself: with authUrl or authCallback the SDK re-fetches on every reconnect
already.
Not the console token
Two unrelated HS256 JWTs live in this system and they share nothing but the algorithm name.
| this page — data plane | the console — control plane | |
|---|---|---|
| held by | a customer's browser or client | a human signed into /app |
| signed with | the API key secret | the console's own secret |
| identified by | header kid | iss: "clowk" |
| carries | x-blackevin-capability, x-blackevin-client-id | sub, email, session_id |
Never pass a console token to a Blackevin client, and never let an API key secret verify a console token.