Authentication¶
Every WebSocket connection is authenticated with a JSON Web Token (JWT) access token during the handshake. After the handshake, message-level authorization is derived from the role carried inside the token.
Reference: connection.md · protocol.md · errors.md
JWT authentication¶
- Token type: access token (JWT claim
type: "access"). - The token must be issued by the Dichit authentication service (the same tokens used by the REST API).
- Refresh tokens are not accepted on the WebSocket. Use the REST refresh endpoint to obtain a new access token, then reconnect.
- The token is verified with the platform signing key; signature, expiry, issuer, and audience are checked.
Token claims used by the socket¶
| Claim | Type | Description |
|---|---|---|
userId | string | The authenticated user id (SUBSCRIBER id or company/admin user id). |
role | SUBSCRIBER \| COMPANY \| SUPERADMIN | The connection's role. |
type | string | Must be "access". |
exp | number | Expiry (epoch seconds). Connections cannot continue past expiry without reconnecting. |
Authentication flow¶
Authentication happens in the upgrade handshake, before the socket is fully open:
- The client opens a WebSocket to
/ws, supplying a valid access token either asAuthorization: Bearer <token>or as theSec-WebSocket-Protocol: websocket.v1, bearer.<token>subprotocol. - The server parses and verifies the JWT access token.
- For
role === 'COMPANY', the server invokesauthorizationService.resolveActor(payload.userId)to verify that the user holds an activeCompanyMembership. The canonicalCompany.idis resolved and attached to the socket context ascompanyId(never the user'suserId). If the membership is not active (missing, suspended, or revoked), the connection is rejected. - On success, the server builds a typed
SocketUser(id,role,companyId,permissions) and completes the 101 upgrade handshake. - On failure (invalid token, expired token, or inactive company membership), the server closes the connection with close code
1008and reasonAuthentication failed. No messages are processed.
sequenceDiagram
participant C as Client
participant S as WebSocket Server
participant J as JWT Service
C->>S: Upgrade request + Bearer token
S->>J: verifyAccessToken(token)
alt valid access token
J-->>S: decoded claims (userId, role)
S-->>C: 101 Switching Protocols (open)
Note over C,S: authenticated socket context
else invalid / expired / refresh token
J-->>S: verification error
S--xC: close 1008 "Authentication failed"
end Authentication success¶
GET /ws HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: websocket.v1, bearer.eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Protocol: websocket.v1
The connection is then ready. There is no auth.login message in the current build; authentication is completed by the handshake. See events/auth.md.
Authentication failure¶
When the token is missing, malformed, expired, or not an access token, the server closes the socket immediately:
Example of a failure (browser-visible as an error event):
Token expiration¶
- Access tokens expire. The socket does not silently refresh them.
- Once the token expires, the platform will terminate the connection (or the client's next operation will be treated as unauthenticated).
- Clients must refresh before expiry and reconnect. Obtain a fresh access token via the REST refresh endpoint, then open a new socket; then re-subscribe (rooms and auctions) and resume using
lastSequencewhere applicable.
Recommended timing: refresh when the token's remaining lifetime drops below a safety margin, and proactively reopen the socket during a quiet period.
Re-authentication¶
Re-authentication is connection-level, not message-level:
- If a connection drops, close it and open a new one with a fresh token.
- There is no in-band re-login command in the current build (
auth.loginis a planned in-band event; see events/auth.md). - After reconnecting, rejoin rooms and re-subscribe to auctions; use
auction.state.snapshotandlastSequencereplay to restore state.
Authorization model¶
Authorization is message-level. Each socket route declares the permissions it requires. The server derives the connection's permission set from its role at handshake time and checks every message against the route's required permissions.
- If the connection lacks any required permission, the server replies with
FORBIDDEN(code: "FORBIDDEN"). - Unknown event types reply with
BAD_REQUEST. - Permission checks run after validation and rate limiting, before the handler.
The pipeline is documented in protocol.md and diagrammed in diagrams.md.
Socket identity¶
interface SocketUser {
readonly id: string; // user id
readonly role: Role; // SUBSCRIBER | COMPANY | SUPERADMIN
readonly companyId?: string; // canonical Company.id resolved from the active CompanyMembership
readonly permissions: readonly string[];
}
For COMPANY connections, companyId is resolved via authorizationService.resolveActor(userId) and attached to the socket context. Room access rules use it to gate the company:<companyId> room.
Roles¶
The platform exposes four client personas over one endpoint. They map to the three token roles as follows:
| Persona | Token role | Description |
|---|---|---|
| Subscriber | SUBSCRIBER | Auction participant who bids and observes live auctions. |
| Company / Auctioneer | COMPANY | Auctioneer staff who run, moderate, and close auctions, and place offline bids. |
| Admin | SUPERADMIN | Platform administrator with staff powers plus admins room access and global broadcasts. |
Each persona connects to the same wss://api.example.com/ws endpoint; only the derived permissions differ.
Permissions¶
Permissions are role-derived and fixed at handshake time:
| Permission | SUBSCRIBER | COMPANY | SUPERADMIN | Effect |
|---|---|---|---|---|
auction:subscribe | ✅ | ✅ | ✅ | Join live auction rooms (auction.subscribe). |
auction:bid:place | ✅ | ✅ | ✅ | Place live bids (auction.bid.place — subscribers for themselves; staff on behalf of attendees). |
auction:staff | ❌ | ✅ | ✅ | Run staff commands (auction.status.update, auction.pause, auction.resume, auction.end, auction.bid.mark, auction.winner.declare, auction.winner.record_lot). |
broadcast:receive | ❌ | ✅ | ✅ | Receive platform/broadcast events. |
room:join | ✅ | ✅ | ✅ | Join rooms via room.join. |
room:leave | ✅ | ✅ | ✅ | Leave rooms via room.leave. |
Notes:
- A missing permission produces
FORBIDDEN, neverUNAUTHORIZED. The connection is authenticated; it simply lacks authority. - The permission model is additive and may grow over time. New permissions are introduced with versioned documentation (see versioning.md).
Permission check example¶
A SUBSCRIBER sending auction.pause:
{
"id": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
"type": "error",
"version": "1.0",
"source": "dichit-backend",
"timestamp": "2026-08-05T10:00:00.000Z",
"sequence": 1,
"data": {
"code": "FORBIDDEN",
"message": "You are not authorized to perform this WebSocket action."
}
}
Request/response examples¶
Handshake (header transport):
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJzdWJfMDFIUTVCWFdZUCIsInJvbGUiOiJTVUJTQ1JJQkVSIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDE2NjQwMH0.signature
Handshake (browser-compatible subprotocol transport):
Related documents¶
- events/auth.md — planned in-band
auth.login/auth.logout. - events/system.md — unauthenticated
system.ping. - rooms.md — room-level access rules that build on these permissions.
- diagrams.md — authorization flow diagram.