Files
projects/Dockers/socket-broker/README.md
T
Bram 3dec007000
Build and Push Docker Images / build-and-push (push) Successful in 1m27s
Secure socket broker with token auth and rooms
2026-10-27 09:35:09 +00:00

180 lines
5.1 KiB
Markdown

# Socket Broker
A Socket.IO broker with topic-based messaging for ComedyKit. Backend services publish over an HTTP API protected by an API key; browsers and mobile clients authenticate with short-lived, topic-scoped tokens issued by Laravel.
## Features
- Socket.IO server for real-time client subscriptions.
- HTTP API for backend publishing.
- API-key authentication for publishers.
- HMAC-SHA256 client tokens for connection and topic authorization.
- Room-based topic delivery (no in-memory subscription maps).
- Protected topic introspection and public health check.
- CORS allowlist, subscription limits, and subscribe rate limiting.
## Architecture
```
Frontend / mobile clients
│ Socket.IO (connection token + topic token)
▼
Socket broker ── rooms: user/{uuid}/notifications
▲ event/{slug}/ticket_statistics
│ POST /publish (API key)
Laravel backend
```
## Configuration
Copy `env.example` to `.env`:
| Variable | Default | Description |
| --- | --- | --- |
| `WS_PORT` | `3000` | Socket.IO / HTTP port |
| `API_PORT` | `WS_PORT` | Optional second HTTP API port |
| `API_KEY` | `default-api-key-change-me` | Publisher API key; never expose to clients |
| `SOCKET_TOKEN_SECRET` | — | Shared HMAC secret; must match Laravel `SOCKET_TOKEN_SECRET` |
| `AUTH_MODE` | `required` | `off`, `optional` (rollout only), or `required` |
| `ALLOWED_ORIGINS` | `https://*.comedykit.be,...` | Comma-separated origins; supports `*.domain.tld` |
| `MAX_TOPICS_PER_SOCKET` | `20` | Subscription cap per connection |
| `SUBSCRIBE_RATE_LIMIT` | `30` | Subscribe attempts per 10 seconds per connection |
`AUTH_MODE=required` refuses to start without `SOCKET_TOKEN_SECRET`.
### Rollout
1. Deploy with `AUTH_MODE=optional` so legacy clients still work.
2. Deploy Laravel with `/notifications/socket-token` and the new publisher job.
3. Deploy the updated web/mobile clients.
4. Switch to `AUTH_MODE=required`.
## Client authentication
Laravel issues two token types:
```json
// Connection token
{ "v": 1, "sub": "<user uuid>", "typ": "connect", "iat": 0, "exp": 0, "jti": "..." }
// Topic token
{ "v": 1, "sub": "<user uuid>", "typ": "topic", "topic": "event/slug/ticket_statistics", "iat": 0, "exp": 0, "jti": "..." }
```
Tokens are encoded as:
```
base64url(JSON payload) + "." + base64url(HMAC-SHA256(payload, SOCKET_TOKEN_SECRET))
```
Clients connect with the connection token:
```javascript
const socket = io("https://bram-sockets.comedykit.be", {
auth: { token: connectionToken },
});
```
A user may always subscribe to `user/{their-own-id}/notifications`. Any other topic requires a topic token whose `sub` matches the connection subject and whose `topic` matches the subscription exactly.
## WebSocket API
### Subscribe
```javascript
socket.emit("subscribe", "user/<uuid>/notifications");
// Or, for a topic-scoped token:
socket.emit("subscribe", {
topic: "event/my-event/ticket_statistics",
token: topicToken,
});
socket.on("subscribed", data => {
console.log(`Subscribed to ${data.topic}`);
});
```
### Unsubscribe
```javascript
socket.emit("unsubscribe", "user/<uuid>/notifications");
socket.on("unsubscribed", data => {
console.log(`Unsubscribed from ${data.topic}`);
});
```
### Receive messages
```javascript
socket.on("message", data => {
console.log(data.topic, data.message, data.data, data.timestamp);
});
```
### Errors
```javascript
socket.on("error", error => {
// INVALID_TOPIC | FORBIDDEN_TOPIC | RATE_LIMITED | TOPIC_LIMIT
console.error(error.code, error.message);
});
```
## HTTP API
### Public
- `GET /health` — status, connection count, auth mode. Does not expose topic names.
### Protected (API key)
- `GET /topics`
- `GET /topics/:topic`
- `POST /publish`
- `POST /publish/:topic`
API key is accepted as `Authorization: Bearer <key>`, `x-api-key`, or `api_key` query parameter.
#### Publish
```bash
curl -X POST http://localhost:3000/publish \
-H "x-api-key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"topic":"user/<uuid>/notifications","message":"{\"id\":\"...\"}"}'
```
Response:
- `200` with `{ success, message, subscribers, payload }`
- `204` when the topic has no subscribers
- `400` invalid request
- `401` invalid or missing API key
## Development
```bash
npm install
npm test
AUTH_MODE=optional API_KEY=dev-key SOCKET_TOKEN_SECRET=dev-secret npm start
```
## Docker
```bash
cp env.example .env
# set API_KEY, SOCKET_TOKEN_SECRET, AUTH_MODE, ALLOWED_ORIGINS
docker compose up -d
```
The Docker image runs as a non-root user and exposes port 3000. If `API_PORT` differs from `WS_PORT`, a second HTTP server is started on that port.
## Security notes
- `API_KEY` is publisher-only. Never ship it to clients.
- `SOCKET_TOKEN_SECRET` must be at least 32 random bytes and identical in Laravel and the broker.
- Tokens are short-lived. The broker verifies them when a subscription is created; clients re-fetch tokens on reconnect.
- Use HTTPS/WSS in production.
- Rotate secrets by coordinating Laravel and broker deployments; outstanding tokens become invalid.
- `AUTH_MODE=optional` or `off` is for local development or the rollout window only.