All checks were successful
Queue Release Build / prepare (push) Successful in 25s
Deploy Web Apps / deploy (push) Successful in 7m8s
Queue Release Build / build-windows (push) Successful in 28m10s
Queue Release Build / build-linux (push) Successful in 44m38s
Queue Release Build / build-android (push) Successful in 18m36s
Queue Release Build / finalize (push) Successful in 1m40s
125 lines
7.1 KiB
Markdown
125 lines
7.1 KiB
Markdown
# Authentication
|
|
|
|
Session-token authentication for the signaling server and product client.
|
|
|
|
## Trust boundaries
|
|
|
|
| Surface | Identity proof | Notes |
|
|
|---|---|---|
|
|
| Signaling server REST (mutations) | `Authorization: Bearer <token>` | Actor user IDs in request bodies are ignored; server derives `authUserId` from the token |
|
|
| Signaling server REST (discovery) | None | `GET /api/servers`, featured/trending/search remain public |
|
|
| Signaling server WebSocket | `identify.token` | Connections must identify before any other message type |
|
|
| Electron Local API | Separate in-memory bearer tokens | Proxies login to allowed signaling servers only |
|
|
| Product client local DB | OS user account | SQLite and attachments are plaintext at rest |
|
|
|
|
## Login / register response
|
|
|
|
```json
|
|
{
|
|
"id": "<uuid>",
|
|
"username": "alice",
|
|
"displayName": "Alice",
|
|
"token": "<opaque-hex>",
|
|
"expiresAt": 1710000000000
|
|
}
|
|
```
|
|
|
|
- Tokens are opaque 64-character hex strings stored in server SQLite (`session_tokens`).
|
|
- Default TTL: 10 years (`SESSION_TOKEN_TTL_MS` env override supported on the signaling server).
|
|
- Passwords are stored with bcrypt; legacy SHA-256 hashes are upgraded transparently on successful login.
|
|
|
|
## Protected REST routes
|
|
|
|
Require `Authorization: Bearer`:
|
|
|
|
- `PUT/POST/DELETE` under `/api/servers/*` (except public `GET`)
|
|
- `PUT /api/requests/:id`
|
|
- Plugin-support mutations under `/api/servers/:serverId/plugins/*`
|
|
- `/api/users/device-tokens/*`
|
|
- `POST /api/users/logout`
|
|
|
|
## WebSocket identify contract
|
|
|
|
```json
|
|
{
|
|
"type": "identify",
|
|
"token": "<session-token>",
|
|
"oderId": "<user-id>",
|
|
"displayName": "Alice",
|
|
"connectionScope": "ws://host:3001",
|
|
"clientInstanceId": "<per-install-uuid>"
|
|
}
|
|
```
|
|
|
|
- `oderId` must match the token's user id when provided.
|
|
- `clientInstanceId` is a stable per-tab UUID generated by the product client (`metoyou.clientInstanceId` in `sessionStorage`). The signaling server uses it to distinguish multiple WebSocket connections for the same user and to route voice ownership.
|
|
- Server responds with `auth_error` or `auth_required` when authentication fails.
|
|
|
|
## Multi-device sessions
|
|
|
|
- Each login/register issues a **new** session token; prior tokens remain valid until they expire or the client calls `POST /api/users/logout` with that token.
|
|
- The same user may keep multiple WebSocket connections open (different devices or browser profiles). Server broadcasts (chat, typing, voice state, status) exclude only the **sending connection**, so other connections for that identity still receive updates.
|
|
- Voice/WebRTC is exclusive per user: only one `clientInstanceId` may own active voice at a time. Other connections show passive UI and can send `voice_client_takeover` to move voice to the local device.
|
|
- Stale reconnect hygiene: when a client re-identifies with the same `(oderId, connectionScope, clientInstanceId)` tuple, the server closes the older socket for that tuple.
|
|
|
|
### Account-owned state sync (`account_sync`)
|
|
|
|
When the same account is logged in on multiple devices, account-owned data is kept in sync through the signaling server:
|
|
|
|
| Data | Mechanism |
|
|
|---|---|
|
|
| Server chat messages | Existing `chat_message` relay (connection-scoped broadcast) |
|
|
| Voice / typing | Existing `voice_state` / `user_typing` relays |
|
|
| Saved servers (join/leave) | `account_sync` payload `saved-room-sync` / `saved-room-remove` |
|
|
| Profile avatar + card text | `account_sync` `user-avatar-full` + `user-avatar-chunk` |
|
|
| Custom emoji library | `account_sync` `custom-emoji-full` + `custom-emoji-chunk` |
|
|
| Friends list | `account_sync` `friend-added` / `friend-removed` |
|
|
| Server icons, edits, reactions | `account_sync` relay of existing P2P broadcast event types |
|
|
|
|
Client rules:
|
|
|
|
- `broadcastMessage()` still fans out over peer data channels; relayable events are **also** wrapped in `account_sync` and sent on the WebSocket.
|
|
- The server forwards `account_sync` to every other open connection for the same `oderId` via `notifyOtherConnectionsForOderId`.
|
|
- Receivers ignore payloads whose `clientInstanceId` matches the local tab id.
|
|
- When a new device identifies, the server notifies existing connections with `account_sync_peer_online`; those devices push a full snapshot (saved rooms, friends, profile, emoji library).
|
|
|
|
WebSocket envelope:
|
|
|
|
```json
|
|
{
|
|
"type": "account_sync",
|
|
"clientInstanceId": "<per-tab-uuid>",
|
|
"payload": { "type": "saved-room-sync", "room": { "...": "..." } }
|
|
}
|
|
```
|
|
|
|
Server response to other connections includes `fromUserId` set to the sender's `oderId`.
|
|
|
|
## Client storage
|
|
|
|
The product client stores tokens per signaling-server base URL in `localStorage` (`metoyou.authTokens`). An HTTP interceptor attaches the bearer token to `/api/*` requests targeting that server.
|
|
|
|
Per-server credentials (`metoyou.signalServerCredentials`) map each normalized signal-server URL to the authenticated user id, username, display name, session token, expiry, and whether the account was auto-provisioned. The home user profile in SQLite/NgRx remains the device-local identity (`homeSignalServerUrl`); foreign-server credentials are a side map used for REST and WebSocket identify on that URL.
|
|
|
|
A per-install **provision secret** enables silent account creation on newly added or encountered signal servers. It is generated on home register/login, stored in Electron `safeStorage` when available (sessionStorage fallback on web), and never persisted as the user's visible login password.
|
|
|
|
### Multi-signal-server auth flows
|
|
|
|
| Flow | Action | Effect |
|
|
|---|---|---|
|
|
| Home login/register | `authenticateUser` | Resets local state, stores home credential + provision secret |
|
|
| Foreign login/register | `authorizeSignalServer` | Upserts credential for that URL only; home session unchanged |
|
|
| Auto-provision | `SignalServerProvisionerService` | Registers or logs in on foreign server using provision secret; on username collision tries suffixed username (`alice-<homeUserIdPrefix>`) |
|
|
| Foreign auth failure | `signalServerAuthFailed` | Clears that URL's credential and re-provisions when home token is still valid; global logout only when home server rejects auth |
|
|
|
|
Authorize UI: `/login?mode=authorize&serverId=…&returnUrl=…` (also supported on `/register`). Settings → Network shows per-endpoint `Authorized` / `Needs sign-in` badges.
|
|
|
|
Persisted local user state (`metoyou_currentUserId` + IndexedDB/SQLite profile) is **not** sufficient to use chat or presence. On startup, `loadCurrentUser$` requires a non-expired session token for the user's home signaling server (or any stored token as a fallback). Missing or rejected **home** tokens dispatch `SESSION_EXPIRED` and redirect to `/login`. Foreign-server `auth_required` / `auth_error` responses clear only that server's credential and attempt re-provision.
|
|
|
|
## Security considerations
|
|
|
|
- Rate limits: login/register (100 / 15 min), server join (30 / min).
|
|
- CORS allowlist: optional `corsAllowlist` in `server/data/variables.json` or `CORS_ALLOWLIST` env (comma-separated). Empty allowlist keeps permissive CORS for local development.
|
|
- Push-token routes require bearer auth and user-id match.
|
|
- RTC relay: direct-message/direct-call types always relay; server-icon types require shared server membership; WebRTC offer/answer/ice remain open for cross-server DM WebRTC.
|