music-assistant-server
22.8 KB•MD
README.md
22.8 KB • 552 lines • markdown
1# Webserver and Authentication Architecture
2
3This document provides a comprehensive overview of the Music Assistant webserver architecture, authentication system, and remote access capabilities.
4
5## Table of Contents
6
7- [Overview](#overview)
8- [Core Components](#core-components)
9- [Authentication System](#authentication-system)
10- [Remote Access (WebRTC)](#remote-access-webrtc)
11- [Request Flow](#request-flow)
12- [Security Considerations](#security-considerations)
13- [Development Guide](#development-guide)
14
15## Overview
16
17The Music Assistant webserver is a core controller that provides:
18- WebSocket-based real-time API for bidirectional communication
19- HTTP/JSON-RPC API for simple request-response interactions
20- User authentication and authorization system
21- Frontend hosting (Vue-based PWA)
22- Remote access via WebRTC for external connectivity
23- Home Assistant integration via Ingress
24
25The webserver runs on port `8095` by default and can be configured via the webserver controller settings.
26
27## Core Components
28
29### 1. WebserverController ([controller.py](controller.py))
30
31The main orchestrator that manages:
32- HTTP server setup and lifecycle
33- Route registration (static files, API endpoints, auth endpoints)
34- WebSocket client management
35- Authentication manager initialization
36- Remote access manager initialization
37- Home Assistant Supervisor announcement (when running as add-on)
38
39**Key responsibilities:**
40- Serves the frontend application (PWA)
41- Hosts the WebSocket API endpoint (`/ws`)
42- Provides HTTP/JSON-RPC API endpoint (`/api`)
43- Manages authentication routes (`/login`, `/auth/*`, `/setup`)
44- Serves API documentation (`/api-docs`)
45- Handles image proxy and audio preview endpoints
46
47### 2. AuthenticationManager ([auth.py](auth.py))
48
49Handles all authentication and user management:
50
51**Database Schema:**
52- `users` - User accounts with roles (admin/user)
53- `user_auth_providers` - Links users to authentication providers (many-to-many)
54- `auth_tokens` - Access tokens with expiration tracking
55- `settings` - Schema version and configuration
56
57**Authentication Providers:**
58- **Built-in Provider** - Username/password authentication with bcrypt hashing
59- **Home Assistant OAuth** - OAuth2 flow for Home Assistant users (auto-enabled when HA provider is configured)
60
61**Token Types:**
62- **Short-lived tokens**: Auto-renewing on use, 30-day sliding expiration window (for user sessions)
63- **Long-lived tokens**: No auto-renewal, 10-year expiration (for integrations/API access)
64
65**Security Features:**
66- Rate limiting on login attempts (progressive delays)
67- Password hashing with bcrypt and user- and server specific salts
68- Secure token generation with secrets.token_urlsafe()
69- WebSocket disconnect on token revocation
70- Session management and cleanup
71
72**User Roles:**
73- `ADMIN` - Full access to all commands and settings
74- `USER` - Standard access (configurable via player/provider filters)
75- `GUEST` - Read-only library access plus player/queue control
76- `SERVICE` - Standard access plus player config, reading user accounts and impersonation
77 (used by the Home Assistant integration)
78
79### 3. RemoteAccessManager ([remote_access/](remote_access/))
80
81Manages WebRTC-based remote access for external connectivity:
82
83**Architecture:**
84- **Signaling Server**: Cloud-based WebSocket server for WebRTC signaling (hosted at `wss://signaling.music-assistant.io/ws`)
85- **WebRTC Gateway**: Local component that bridges WebRTC data channels to the WebSocket API
86- **Remote ID**: Unique identifier (format: `MA-XXXX-XXXX`) for connecting to specific instances
87
88**How it works:**
891. Remote access can be enabled regardless of Home Assistant Cloud subscription
902. A unique Remote ID is generated and stored in config
913. The gateway connects to the signaling server and registers with the Remote ID
924. Remote clients (PWA or mobile apps) connect via WebRTC using the Remote ID
935. Data channel messages are bridged to/from the local WebSocket API
94
95**Connection Modes:**
96
97- **Basic Mode** (default, no HA Cloud required):
98 - Uses public STUN servers (Home Assistant, Google, Cloudflare)
99 - Works in most network configurations
100 - May not work behind complex NAT setups or corporate firewalls
101 - Free for all users
102
103- **Optimized Mode** (with HA Cloud subscription):
104 - Uses Home Assistant Cloud STUN/TURN servers
105 - Reliable connections in all network configurations
106 - TURN relay servers ensure connectivity even in restrictive networks
107 - Requires active Home Assistant Cloud subscription
108
109**Key features:**
110- Automatic reconnection on signaling server disconnect
111- Multiple concurrent WebRTC sessions supported
112- No port forwarding required
113- End-to-end encryption via WebRTC (DTLS-SRTP)
114- Automatic mode switching when HA Cloud status changes
115
116### 4. WebSocket Client Handler ([websocket_client.py](websocket_client.py))
117
118Manages individual WebSocket connections:
119- Authentication enforcement (auth or login command must be first)
120- Command routing and response handling
121- Event subscription and broadcasting
122- Connection lifecycle management
123- Token validation and user context
124
125### 5. Authentication Helpers
126
127**Helpers ([helpers/auth_middleware.py](helpers/auth_middleware.py)):**
128- Request authentication for HTTP endpoints, called per handler (there is no aiohttp middleware)
129- User context management (thread-local storage)
130- Ingress detection (Home Assistant add-on)
131- Token extraction from Authorization header
132
133**Providers ([helpers/auth_providers.py](helpers/auth_providers.py)):**
134- Base classes for authentication providers
135- Built-in username/password provider
136- Home Assistant OAuth provider
137- Rate limiting implementation
138
139## Authentication System
140
141### First-Time Setup Flow
142
1431. **Initial State**: No users exist
1442. **Setup Required**: User is redirected to `/setup`
1453. **Admin Creation**: User creates the first admin account with username/password
1464. **Setup completes** User gets redirected to the frontend
1475. **Onboarding wizard** The frontend shows the onboarding wizard if it detects 'onboard_done' is False
1484. **Onboarding Complete**: User completes onboarding and the `onboard_done` flag is set to `true`
149
150### First-Time Setup Flow when HA Ingress is used
151
1521. **Initial State**: No users exist
1532. **Auto user creation**: User is auto created based on HA user
1544. **Setup completes** User gets redirected to the frontend
1555. **Onboarding wizard** The frontend shows the onboarding wizard if it detects 'onboard_done' is False
1564. **Onboarding Complete**: User completes onboarding and the `onboard_done` flag is set to `true`
157
158### Login Flow (Standard)
159
1601. **Client Request**: POST to `/auth/login` with credentials
1612. **Provider Authentication**: Credentials validated by authentication provider
1623. **Token Generation**: Short-lived token created for the user
1634. **Response**: Token and user info returned to frontend
1645. **Subsequent Requests**: Token included in Authorization header or WebSocket auth command
165
166### Login Flow (Home Assistant OAuth)
167
1681. **Initiate OAuth**: GET `/auth/authorize?provider_id=homeassistant&return_url=...`
1692. **Redirect to HA**: User is redirected to Home Assistant OAuth consent page
1703. **OAuth Callback**: HA redirects back to `/auth/callback` with code and state
1714. **Token Exchange**: Code exchanged for HA access token
1725. **User Lookup/Creation**: User found or created with HA provider link
1736. **Token Generation**: MA token created and returned via redirect with `code` parameter
1747. **Client Handling**: Client extracts token from URL and stores it
175
176### Remote Client OAuth Flow
177
178For remote clients (PWA over WebRTC), OAuth requires special handling since redirect URLs can't point to localhost:
179
1801. **Request Session**: Remote client calls `auth/authorization_url` with `for_remote_client=true`
1812. **Session Created**: Server creates a pending OAuth session and returns session_id and auth URL
1823. **User Opens Browser**: Client opens auth URL in system browser
1834. **OAuth Flow**: User completes OAuth in browser
1845. **Token Stored**: Server stores token in pending session (using special return URL format)
1856. **Polling**: Client polls `auth/oauth_status` with session_id
1867. **Token Retrieved**: Once complete, client receives token and can authenticate
187
188### Ingress Authentication (Home Assistant Add-on)
189
190When running as a Home Assistant add-on:
191- A dedicated webserver TCP site is hosted (on port 8094) bound to the internal HA docker network only
192- Ingress requests include HA user headers (`X-Remote-User-ID`, `X-Remote-User-Name`)
193- Users are auto-created on first access
194- No password required (authentication handled by HA)
195- System user created for HA integration communication
196
197### WebSocket Authentication
198
1991. **Connection Established**: Client connects to `/ws`
2002. **Auth Command Required**: First command must be `auth` with token
2013. **Token Validation**: Token validated and user context set
2024. **Authenticated Session**: All subsequent commands executed in user context
2035. **Auto-Disconnect**: Connection closed on token revocation or user disable
204
205## Remote Access (WebRTC)
206
207### Architecture Overview
208
209Remote access enables users to connect to their Music Assistant instance from anywhere without port forwarding or VPN:
210
211```
212[Remote Client (PWA or app)]
213 |
214 | WebRTC Data Channel
215 v
216[Signaling Server] ââ [WebRTC Gateway]
217 |
218 | WebSocket
219 v
220 [Local WebSocket API]
221```
222
223### Components
224
225**Signaling Server** (`wss://signaling.music-assistant.io/ws`):
226- Cloud-based WebSocket server for WebRTC signaling
227- Handles SDP offer/answer exchange
228- Routes ICE candidates between peers
229- Maintains Remote ID registry
230
231**WebRTC Gateway** ([remote_access/gateway.py](remote_access/gateway.py)):
232- Runs locally as part of the webserver controller
233- Connects to signaling server and registers Remote ID
234- Accepts incoming WebRTC connections from remote clients
235- Bridges WebRTC data channel messages to local WebSocket API
236- Handles multiple concurrent sessions
237
238**Remote ID**:
239- Format: `MA-XXXX-XXXX` (e.g., `MA-K7G3-P2M4`)
240- Uniquely identifies a Music Assistant instance
241- Generated once and stored in controller config
242- Used by remote clients to connect to specific instance
243
244### Connection Flow
245
2461. **Initialization**:
247 - Remote access is enabled by user in settings
248 - Remote ID generated/retrieved from config
249 - HA Cloud status checked (determines mode)
250 - Gateway connects to signaling server with appropriate ICE servers
251 - Remote ID registered with signaling server
252
2532. **Remote Client Connection**:
254 - User opens PWA (https://app.music-assistant.io) and enters Remote ID
255 - PWA creates WebRTC peer connection
256 - PWA sends SDP offer via signaling server
257 - Gateway receives offer and creates peer connection
258 - Gateway sends SDP answer via signaling server
259 - ICE candidates exchanged for NAT traversal
260 - WebRTC data channel established
261
2623. **Message Bridging**:
263 - Remote client sends WebSocket-format messages over data channel
264 - Gateway forwards messages to local WebSocket API
265 - Responses and events sent back through data channel
266 - Authentication and authorization work identically to local WebSocket
267
268### Data Channels
269
270A single remote session multiplexes several WebRTC data channels over one peer connection.
271The gateway routes each incoming channel by its label through a label -> handler table, with
272two kinds of handlers:
273
274- **Bridged**: the channel is pumped both ways to a local WebSocket
275 - `sendspin`: the built-in Sendspin server (web player)
276 - `live_announcement`: the live announcement route on the local webserver
277- **Served in-process**: handled by the gateway itself, without a local WebSocket
278 - `http_proxy`: proxied HTTP requests (album art and other assets)
279
280When one of these channels or its local WebSocket closes, only that channel is torn down and
281the session stays up.
282
283The client's own API channel has no fixed label: the **first** channel with a label the server
284does not recognise becomes the API channel (the frontend labels it `ma-api`) and is bridged to
285`/ws`. Any **later** unrecognised label is refused, since taking it for a second API channel
286would replace the live bridge and break the session. The API channel shares its lifetime with
287the session: when it or its local WebSocket closes, the whole session is torn down.
288
289Proxied HTTP requests are answered on the channel they arrived on. That is what keeps older
290clients working: they send `http-proxy-request` over `ma-api` and get the response back there,
291so the gateway needs no version negotiation of its own.
292
293The reply is framed to suit that channel. On `ma-api` it is one JSON message with the body
294hex-encoded, which costs about 2.7x the image once the oversized-message chunking below is
295applied on top. `http_proxy` carries nothing else, so there the reply is a JSON header
296(`type`, `id`, `status`, `headers`, `size`) followed by the body as raw binary messages â the
297image costs its own size and no more. Those binary messages carry no request id, so the
298gateway holds the channel for a whole reply: replies go out one at a time rather than
299interleaving, which a channel that sends one message at a time would do anyway. A client that
300stops draining is given a bounded time per frame, after which the reply is abandoned where it
301stands â so a reply can end short of its announced `size`, and the next header is what follows.
302
303`ma-api` and `http_proxy` size their bulk frames to the channel's `max_message_size`, the lower of
304our own 256 KiB ceiling and what the peer advertises in its SDP â and libdatachannel assumes
305only 64 KiB when it advertises nothing. On `http_proxy` that bounds the binary body frames. On
306`ma-api` any message over 64 KiB â or over the cap, whichever is lower â is split into
307`__chunk__` frames (`id`, `seq`, `count`, `b64`) the client reassembles by group id. A client
308therefore has to expect chunking well before the cap: pieces are 64 KiB by preference, sized
309down only when the cap cannot fit that much base64 plus the frame's JSON envelope.
310
311**Adding a new label** is not backwards compatible by itself: servers from before the routing
312table mistake an unknown label for the API channel, which breaks the entire remote session
313instead of just the new feature. A client must therefore feature-detect on `schema_version`
314from `server_info` before opening one: `http_proxy` requires `API_SCHEMA_VERSION >= 49`. Bump
315`API_SCHEMA_VERSION` ([constants.py](../../constants.py)) when adding a label and gate the
316client on the new value.
317
318### ICE Servers (STUN/TURN)
319
320NAT traversal is critical for WebRTC connections. Music Assistant uses:
321
322- **STUN servers**: Servers for discovering public IP addresses and port mappings
323- **TURN servers**: Relay servers for cases where direct peer-to-peer connection fails
324
325**Basic Mode (Public STUN):**
326- `stun:stun.home-assistant.io:3478` (Home Assistant public STUN)
327- `stun:stun.l.google.com:19302` (Google public STUN)
328- `stun:stun1.l.google.com:19302` (Google public STUN backup)
329- `stun:stun.cloudflare.com:3478` (Cloudflare public STUN)
330
331Most connections succeed with public STUN servers alone, but they may fail in:
332- Symmetric NAT configurations
333- Corporate firewalls that block UDP
334- Networks with restrictive firewall policies
335
336**Optimized Mode (HA Cloud):**
337- STUN/TURN servers provided by Home Assistant Cloud
338- Includes TURN relay servers for guaranteed connectivity
339
340### Availability
341
342Remote access is available to all users:
343- **Basic Mode**: Always available, no subscription required
344- **Optimized Mode**: Requires active Home Assistant Cloud subscription
345
346### API Endpoints
347
348**`remote_access/info`** (WebSocket command):
349Returns remote access status:
350```json
351{
352 "enabled": true,
353 "running": true,
354 "connected": true,
355 "remote_id": "MA-K7G3-P2M4",
356 "using_ha_cloud": false,
357 "signaling_url": "wss://signaling.music-assistant.io/ws"
358}
359```
360
361**`remote_access/configure`** (WebSocket command, admin only):
362Enable or disable remote access:
363```json
364{
365 "enabled": true
366}
367```
368
369## Request Flow
370
371### HTTP Request Flow
372
373```
374HTTP Request â Webserver â Command Handler â Response
375 |
376 ââ get_authenticated_user()
377 ââ Ingress? â Auto-authenticate with HA headers
378 ââ Regular? â Validate Bearer token
379```
380
381### WebSocket Request Flow
382
383```
384WebSocket Connect â WebsocketClientHandler
385 |
386 ââ First command: auth â Validate token â Set user context
387 ââ Subsequent commands â Check auth/role â Execute â Respond
388```
389
390### Remote WebRTC Request Flow
391
392```
393Remote Client â WebRTC Data Channel â Gateway â Local WebSocket API
394 |
395 ââ Message forwarding (bidirectional)
396```
397
398## Security Considerations
399
400### Authentication
401
402- **Mandatory authentication**: All API access requires authentication (except Ingress)
403- **Secure token generation**: Uses `secrets.token_urlsafe(48)` for cryptographically secure tokens
404- **Password hashing**: bcrypt with user-specific salts
405- **Rate limiting**: Progressive delays on failed login attempts
406- **Token expiration**: Both short-lived (30 days sliding) and long-lived (10 years) tokens supported
407
408### Authorization
409
410- **Role-based access**: Admin vs User roles
411- **Command-level enforcement**: API commands can require specific roles
412- **Player/Provider filtering**: Users can be restricted to specific players/providers
413- **Token revocation**: Immediate WebSocket disconnect on token revocation
414
415### Network Security
416
417**Local Network:**
418- Webserver is unencrypted (HTTP) by design (runs on local network)
419- Users should use reverse proxy or VPN for external access
420- Never expose webserver directly to internet
421
422**Remote Access:**
423- End-to-end encryption via WebRTC (DTLS/SRTP)
424- Authentication required (same as local access)
425- Signaling server only routes encrypted signaling messages
426- Cannot decrypt or inspect user data
427
428### Data Protection
429
430- **Token storage**: Only hashed tokens stored in database
431- **Password storage**: bcrypt with user-specific salts
432- **Session cleanup**: Expired tokens automatically deleted
433- **User disable**: Immediate disconnect of all user sessions
434
435## Development Guide
436
437### Adding New Authentication Providers
438
4391. Create provider class inheriting from `LoginProvider` in [helpers/auth_providers.py](helpers/auth_providers.py)
4402. Implement required methods: `authenticate()`, `get_authorization_url()` (if OAuth), `handle_oauth_callback()` (if OAuth)
4413. Register provider in `AuthenticationManager._setup_login_providers()`
4424. Add provider configuration to webserver config entries if needed
443
444### Adding New API Endpoints
445
4461. Define route handler in [controller.py](controller.py) (for HTTP endpoints)
4472. Use `@api_command()` decorator for WebSocket commands (in respective controllers)
4483. Specify authentication requirements: `authenticated=True` and/or `required_scope=Scope.<SCOPE>`
4494. Optionally set `allow_impersonation=True` to let callers execute the command on behalf of
450 another user via the injected `user` argument (requires the `users.impersonate` scope
451 when targeting another user)
452
453### Testing Authentication
454
4551. **Local Testing**: Use `/setup` to create admin user, then `/auth/login` to get token
4562. **HTTP API Testing**: Use curl with `Authorization: Bearer <token>` header
4573. **WebSocket Testing**: Connect to `/ws` and send auth command with token
4584. **Role Testing**: Create users with different roles and test access restrictions
459
460### Common Patterns
461
462**Getting current user in command handler:**
463```python
464from music_assistant.controllers.webserver.helpers.auth_middleware import get_current_user
465
466@api_command("my_command")
467async def my_command():
468 user = get_current_user()
469 if not user:
470 raise AuthenticationRequired("Not authenticated")
471 # ... use user ...
472```
473
474**Getting current token (for revocation):**
475```python
476from music_assistant.controllers.webserver.helpers.auth_middleware import get_current_token
477
478@api_command("my_command")
479async def my_command():
480 token = get_current_token()
481 # ... use token ...
482```
483
484**Requiring a scope:**
485```python
486from music_assistant_models.auth import Scope
487
488@api_command("admin_only_command", required_scope=Scope.CONFIG_CORE_WRITE)
489async def admin_command():
490 # Only users whose role grants the config.core.write scope can call this
491 pass
492```
493
494Scopes are granted to users through their role, see `ROLE_SCOPES` in
495[helpers/auth_middleware.py](helpers/auth_middleware.py) for the builtin role definitions.
496
497### Database Migrations
498
499When modifying the auth database schema:
5001. Increment `DB_SCHEMA_VERSION` in [auth.py](auth.py)
5012. Add migration logic to `_migrate_database()` method
5023. Test migration from previous version
5034. Consider backwards compatibility
504
505### Testing Remote Access
506
5071. **Enable Remote Access**: Toggle remote access in settings UI or via API
5082. **Verify Remote ID**: Check webserver config for generated Remote ID
5093. **Test Gateway**: Check logs for "Starting remote access in basic/optimized mode" message
5104. **Test Connection**: Use PWA with Remote ID to connect externally
5115. **Monitor Sessions**: Check `remote_access/info` command for status and mode
5126. **Test Mode Switching**: Enable/disable HA Cloud and verify automatic mode switching
513
514## File Structure
515
516```
517webserver/
518âââ __init__.py # Module exports
519âââ controller.py # Main webserver controller
520âââ auth.py # Authentication manager
521âââ websocket_client.py # WebSocket client handler
522âââ api_docs.py # API documentation generator
523âââ README.md # This file
524âââ helpers/
525â âââ auth_middleware.py # HTTP/WebSocket auth helpers
526â âââ auth_providers.py # Authentication providers
527âââ remote_access/
528 âââ __init__.py # Remote access manager
529 âââ gateway.py # WebRTC gateway implementation
530```
531
532## Additional Resources
533
534- [API Documentation](http://localhost:8095/api-docs) - Auto-generated API docs
535- [Commands Reference](http://localhost:8095/api-docs/commands) - List of all API commands
536- [Schemas Reference](http://localhost:8095/api-docs/schemas) - Data model documentation
537- [Swagger UI](http://localhost:8095/api-docs/swagger) - Interactive API explorer
538
539## Contributing
540
541When contributing to the webserver/auth system:
5421. Follow the existing patterns for consistency
5432. Add comprehensive docstrings with Sphinx-style parameter documentation
5443. Update this README if adding significant new features
5454. Test authentication flows thoroughly
5465. Consider security implications of all changes
5476. The API documentation will be auto updated if adding new commands (based on docstrings and type hints)
548
549---
550
551*This architecture document is maintained alongside the code and should be updated when significant changes are made to the provider's design or functionality.*
552