Skip to content

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:

  1. The client opens a WebSocket to /ws, supplying a valid access token either as Authorization: Bearer <token> or as the Sec-WebSocket-Protocol: websocket.v1, bearer.<token> subprotocol.
  2. The server parses and verifies the token.
  3. On success, the server builds a typed SocketUser (id, role, company id, permissions) and opens the connection.
  4. On failure, the server closes the connection with close code 1008 and reason Authentication failed. No message is 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:

close code 1008, reason "Authentication failed"

Example of a failure (browser-visible as an error event):

ws.addEventListener('error', event => {
  // readyState is CLOSED, close code 1008
});

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 lastSequence where 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.login is a planned in-band event; see events/auth.md).
  • After reconnecting, rejoin rooms and re-subscribe to auctions; use auction.snapshot and lastSequence replay 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; // set for COMPANY (currently = userId)
  readonly permissions: readonly string[];
}

For COMPANY connections, companyId is currently derived from userId; 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).
auction:staff Run staff commands (auction.status.update, auction.pause, auction.resume, auction.end, bid.mark, winner.declare, 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, never UNAUTHORIZED. The connection is authenticated; it simply lacks authority.
  • The permission model is additive and may grow over time. New permissions are introduced in a backward-compatible way (see versioning.md).

Permission check example

A SUBSCRIBER sending auction.pause:

{
  "type": "error",
  "requestId": "req_01HQ5BXWYP1Y5RX0W5X0W5X0W5X",
  "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):

const ws = new WebSocket('wss://api.example.com/ws', ['websocket.v1', `bearer.${token}`]);