Skip to content

Authentication

Domekologe edited this page Jun 30, 2026 · 1 revision

Authentication

🌐 English · Deutsch

The dashboard authenticates users with Discord OAuth2 and keeps the session in a signed, server-held cookie. The security model has a single guiding principle: secrets never reach the browser. The Discord client secret and the gateway token live only in the SvelteKit server (the BFF), the gateway only accepts connections from localhost plus a token, and every privileged action is re-checked against Red's permission system on the bot side. This page explains the login flow, the session model and the permission levels.

Screenshot: Discord OAuth2 consent screen

The OAuth2 login flow

  1. The user clicks Login with Discord. The BFF (GET /auth/login) generates a random state value, stores it in a short-lived httpOnly cookie, and redirects the browser to Discord's authorize URL with the identify and guilds scopes.
  2. The user approves on Discord and is sent back to DISCORD_REDIRECT_URI (/auth/callback).
  3. The BFF (GET /auth/callback) verifies the returned state against the cookie, then exchanges the authorization code for an access token server-side using DISCORD_CLIENT_SECRET.
  4. With that token, the BFF fetches the Discord user (identify) and derives the UI locale (de-DE for German users, otherwise en-US).
  5. The BFF creates a session and sets the signed session cookie. The browser never sees the client secret or the raw access token handling.
Browser ──/auth/login──▶ BFF ──redirect──▶ Discord
Browser ◀──consent────── Discord
Browser ──/auth/callback (code,state)──▶ BFF ──code→token (secret)──▶ Discord
                                          BFF ◀──user──────────────── Discord
Browser ◀──Set-Cookie: dks_session────── BFF

Sessions (signed cookies, server-side BFF)

The session is stored in a cookie named dks_session. It is not an opaque database key — it is the user payload plus an HMAC-SHA256 signature keyed on SESSION_SECRET:

dks_session = base64url(payload) . base64url(HMAC-SHA256(payload, SESSION_SECRET))
Property Value
Cookie name dks_session
Signature HMAC-SHA256, verified with constant-time comparison
Flags httpOnly, sameSite=lax, secure in production
Lifetime 7 days
Payload user id, username, avatar, locale, issued-at (iat), token data

Because the cookie is signed (not encrypted), it cannot be tampered with: any change to the payload invalidates the HMAC. The signing and verification happen exclusively in the BFF (lib/server/session.ts), so the SESSION_SECRET never leaves the server.

Session invalidation (epoch)

The bot owner can invalidate all sessions at once ("Refresh sessions" in Settings). This sets a session_epoch timestamp on the cog. On every request the BFF compares the session's iat against the current epoch (cached for ~60 s to avoid a round-trip per request) and clears any session older than the epoch.

The gateway: localhost + token

The BFF talks to the bot over the cog's JSON-RPC gateway. Two things protect it:

  • Localhost binding. The gateway listens on 127.0.0.1 by default — it is not reachable from the network.
  • Shared token. Every BFF request carries the X-Dashboard-Token header. The cog compares it in constant time (compare_digest). Only the BFF knows the token.

The browser never talks to the gateway directly. It only ever calls the BFF's own /api/* endpoints, which then call the gateway server-side. See API & Gateway for the protocol.

Users only see their own guilds

When listing servers, the gateway returns only guilds where the user is actually a member (or where they are the bot owner). Even within a guild context, a logged-in non-member cannot read that guild's data: the server forces at least guild_member for any guild-scoped call, so manipulating a guild_id in a request cannot leak another server's content.

Permission levels

Every contribution (widget, panel, list, page) and every privileged RPC method declares a minimum permission level. The gateway resolves the user's effective level against Red's own permission system and enforces it server-side on every call — frontend filtering is only for UX.

Level Value Mapped to Red
authenticated 0 Logged in via Discord OAuth2
guild_member 1 Member of the guild
guild_mod 2 Red's mod role (bot.is_mod)
guild_admin 3 Red's admin role or Discord manage_guild
guild_owner 4 guild.owner_id == user.id
bot_owner 5 bot.is_owner(user)

Levels are cumulative: a higher level satisfies any lower requirement. The mapping lives in the cog's permissions.py.

Security model in one picture

        secrets here ──┐
                       ▼
Browser ──OAuth2──▶ BFF (SvelteKit server) ──token──▶ gateway (localhost) ──▶ Red bot
  cookie (HMAC)         DISCORD_CLIENT_SECRET            compare_digest         is_owner / is_admin / …
                        GATEWAY_TOKEN                                           permission re-check
                        SESSION_SECRET
  • Discord client secret, gateway token and session secret all live only in the BFF.
  • The browser holds only a signed cookie — no usable secret.
  • Permissions are enforced on the bot, not trusted from the frontend.

Related pages

  • ConfigurationSESSION_SECRET, GATEWAY_TOKEN and the OAuth2 variables
  • API & Gateway — how the BFF calls the gateway and where permissions are checked
  • Architecture — the BFF pattern in detail

Clone this wiki locally