@inteli.city/node-red-contrib-websocket-plus 1.0.0
Node-RED WebSocket nodes with proxy support
node-red-contrib-websocket-plus
Node-RED WebSocket nodes with JWT-enforced authentication, multitenant routing, and proxy support.
Drop-in replacement for the built-in Node-RED WebSocket nodes. Node types are prefixed websocket-plus to avoid conflicts with the official nodes. The nodes appear in the palette as ws.in+ and ws.out+.
Table of contents
Core guarantees
- Connections are rejected at the WebSocket handshake if the token is missing, invalid, or expired.
- Tenant identity is derived from a verified JWT claim and is immutable for the lifetime of the connection.
- Message routing enforces tenant boundaries — cross-tenant delivery requires explicit configuration.
- Flows receive pre-verified identity via
msg._session; they do not need to re-authenticate.
Nodes
websocket.in+
Palette label: ws.in+ — type: websocket-plus in
Receives messages from a WebSocket connection and forwards them into the flow. Attach to a websocket-plus-listener (server) or websocket-plus-client (outbound) config node.
Every output message includes:
| Property | Description |
|---|---|
msg.payload |
The received data. |
msg._session |
Verified connection identity established at handshake time. See msg._session. |
websocket.out+
Palette label: ws.out+ — type: websocket-plus out
Sends messages over a WebSocket connection. Routing is determined by the Message scope configured on the attached listener — not by message content.
| Scope | Behaviour |
|---|---|
| Session | Replies only to the originating connection (msg._session.id). |
| Tenant | Broadcasts to all connections sharing the same verified tenant identity. |
| Global | Broadcasts to every connected client regardless of tenant. Use with care. |
Tenant identity cannot be overridden via message fields — routing always uses the verified msg._session.tenantId.
Reply pattern: receive on websocket.in+ → preserve msg._session → send on websocket.out+ with scope set to Session.
websocket-plus-listener (server config node)
Configures a server-side WebSocket endpoint. Authentication and tenant identity are enforced at the HTTP upgrade handshake — connections are rejected before any WebSocket frame is exchanged.
Connection
| Field | Description |
|---|---|
| Path | URL path relative to httpNodeRoot, e.g. /ws/events. Must not start with /debug/ws. |
| Send/Receive | Payload only passes msg.payload; Entire message passes the full JSON object. |
Security — JWT validation
The JWT is always read from the token query parameter. The parameter name is fixed and cannot be changed.
ws://host/path?token=<jwt>
| Field | Description |
|---|---|
| JWT validation | How to verify the token. |
| Shared secret | HMAC secret (HS256 mode only). Stored as a Node-RED credential. |
| JWKS URI | URL of the JWKS endpoint (JWKS mode only), e.g. https://auth.example.com/.well-known/jwks.json. |
| JWT issuer | Expected iss claim. Tokens with a non-matching issuer are rejected. Optional. |
| JWT audience | Expected aud claim. Tokens without a matching audience are rejected. Optional. |
Validation modes:
| Mode | Description |
|---|---|
None |
No token check — any client can connect. Unsafe. |
Shared secret (HS256) |
Token signature verified with a symmetric HMAC-SHA256 secret. |
JWKS endpoint (RS256 / ES256) |
Token signature verified with a public key fetched from a JWKS endpoint. Recommended for production. |
Token expiration (exp) is always checked when present, regardless of mode.
Tenant & scope
| Field | Description |
|---|---|
| Tenant from | Token claim (default) reads a claim from the verified JWT. Static assigns a single hard-coded tenant to all connections. |
| Claim name | JWT claim used as the tenant identifier. Default: tenant_id. Shown when Tenant from is Token claim. |
| Tenant ID | Hard-coded tenant identifier. Shown when Tenant from is Static. |
| Message scope | Broadcast reach for outbound messages: Session only, Tenant-only (default), or Global. |
Advanced
| Field | Description |
|---|---|
| Timeout | Session timeout in seconds. 0 or empty = no limit. |
| On token expiry | Disconnect (default) — terminates the connection when the JWT exp is reached. Allow stale session — keeps the connection alive after expiry (unsafe). |
| Rate limit | Maximum messages per second per connection. Empty = no limit. (Informational — not enforced by the node itself.) |
| Max connections | Maximum concurrent connections per tenant. Empty = no limit. (Informational — not enforced by the node itself.) |
websocket-plus-client (client config node)
Configures an outbound WebSocket connection.
Connection
| Field | Description |
|---|---|
| URL | Remote WebSocket URL (ws:// or wss://). |
| TLS Configuration | Shown only for wss:// URLs. Select a Node-RED TLS config node. |
| Subprotocol | Optional WebSocket sub-protocol string. |
| Send/Receive | Payload only or Entire message. |
Authentication
| Field | Description |
|---|---|
| Auth mode | JWT (default) or None. |
| JWT | The token appended to the connection URL as ?token=<jwt>. Stored as a Node-RED credential and never exported with the flow. |
Auth mode None connects without credentials and will be rejected by any listener with JWT validation enabled.
Advanced
| Field | Description |
|---|---|
| Heartbeat | When enabled, sends a WebSocket ping at the configured interval (seconds). |
| Upgrade headers | Additional HTTP headers sent with the WebSocket upgrade request. Supports static values and environment variables. |
msg._session
Every message produced by a websocket.in+ node includes a _session object populated from the verified JWT claims:
{
"type": "websocket",
"id": "<connection-id>",
"tenantId": "<verified-tenant-id>",
"subject": "<jwt-sub-claim>",
"authStatus": "verified",
"routingScope": "tenant"
}
| Field | Description |
|---|---|
type |
Always "websocket". |
id |
Unique connection ID. Pass this in msg._session to a websocket.out+ node to reply to the originating client. |
tenantId |
Tenant identity derived at connect time from a verified JWT claim (or the static ID). Immutable for the session lifetime. |
subject |
Value of the JWT sub claim, if present. |
authStatus |
"verified" when a JWT was validated; "none" when JWT validation mode is None. |
routingScope |
The scope configured on the listener: "session", "tenant", or "global". |
msg._session is the authoritative source of identity — flows should not recreate or modify it.
Authentication flow
Client → ws://host/path?token=<jwt>
│
▼
verifyClient (ws handshake)
├─ Extract token from query param "token" (fixed)
├─ Verify signature (HS256 / JWKS)
├─ Check exp, iss, aud
├─ Extract tenantId from claim
└─ REJECT (401) on any failure
│
▼ (accepted)
handleConnection
├─ socket.nrAuth = { claims, tenantId }
├─ socket.nrTenantId = tenantId
└─ schedule terminate() at JWT exp (if authExpiry=disconnect)
│
▼ (message received)
msg._session = { id, tenantId, subject, authStatus, routingScope }
│
▼ (websocket.out+)
broadcast filtered by routingScope + tenantId
Proxy support
Outbound client connections respect standard proxy environment variables and Node-RED settings (checked in this order):
RED.settings.proxyOptions.https_proxy/http_proxy/no_proxyhttps_proxy,HTTPS_PROXY,http_proxy,HTTP_PROXY,NO_PROXYenvironment variables
Installation
npm install @inteli.city/node-red-contrib-websocket-plus
License
Apache-2.0