docs/DISCORD_OAUTH.md
Discord OAuth web controls
The web UI uses Discord's authorization-code login flow with only identify to prove the controller's identity. The existing Discord connection separately verifies that user's roles in the active stream's guild. Web login does not replace the streaming account's TOKEN.
- Create an application in the Discord Developer Portal.
- Register the exact redirect URL ending in
/auth/callback. Use HTTPS for remote access; local development can usehttp://localhost:8080/auth/callback. - Set
DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET, andDISCORD_REDIRECT_URIin the server's private environment. Do not commit the secret. - Configure guild role/user grants using
PERMISSIONS_FILE, then setSERVER_ENABLED=true. Unconfigured guilds allow only owners and Administrators. - Start a stream, join its voice channel, open the configured public origin, and choose Sign in with Discord.
The enabled web server rejects incomplete OAuth configuration before Discord login. A login alone grants no browser access: current guild, voice channel, active session, and role/user permissions are checked on every HTTP and Socket.IO action. control covers keys/search/back/refresh/presets; arbitrary URLs require navigate. Session owners with current join permission can control their own session. The UI shows login, denied-access, and expiry messages.
OAuth state is cryptographically random, HMAC-signed, browser-bound, and short-lived. Abandoned login requests allocate no shared pending-login slots. Successful login states cannot be replayed; failed callbacks clear the browser cookie. A Discord account is limited to ten successful logins per five minutes and three concurrent sessions. At total session capacity, the oldest session is evicted instead of blocking new logins. Opaque session cookies are HttpOnly/SameSite=Lax, and Secure for HTTPS; OAuth bearer tokens are used only during the identity lookup and are not retained in web sessions; refresh tokens are not stored. State and sessions are discarded on restart. Sessions expire at the earlier of token expiry or eight hours; expiry requires a fresh login. Logout immediately disconnects that session's sockets.
REST writes require the session's CSRF token and exact public Origin. Socket.IO checks Origin on polling and WebSocket requests and validates the session/CSRF token on connection. Every event rechecks expiry, voice state, current session, and capabilities. Browser URL/preset updates are sent only to currently authorized participants. The existing Discord connection fetches fresh guild roles, with proof cached for at most 30 seconds and refreshes shared across browser sessions; role revocation can take that long, while voice/session changes apply on the next action. If verification is temporarily delayed, the server reports 503, disables controls, and the UI retries automatically. Expired role proof never extends access during a delay.
Native listening and Compose host publishing default to loopback. Keep the published port private behind an HTTPS reverse proxy; set the callback to its public origin. The container listens on 0.0.0.0 internally. HTTP(S) private media servers remain usable, but explicit file:, javascript:, data:, browser-internal URLs and URL credentials are rejected before Selenium.
Automated tests use a stub OAuth provider and real HTTP/Socket.IO transports. Real Discord consent and a deployed callback require a configured application. See Discord's OAuth documentation for the authorization-code grant and scopes.