Skip to content

Authentication & Sessions

Steam OpenID 2.0 via API Gateway — no separate user database needed.


Overview

The QS-Bridge panel uses a three-layer authentication architecture:

  1. API Gateway (panel/api/) — Hono TypeScript server that handles Steam OpenID 2.0 verification and issues signed JWT session cookies
  2. SpacetimeDB Module — stores ModuleConfig.owner_identity (set during init). The gateway connects with the publisher's token, so ctx.sender() matches owner_identity
  3. React Panel (panel/ui/) — connects to STDB via WebSocket for read-only subscriptions. All admin mutations go through the gateway REST API

Authentication Flow

graph TD
    A["User clicks 'Sign in with Steam'"] --> B["1. Browser → GET /auth/steam<br/>Gateway redirects to Steam OpenID login page"]
    B --> C["2. User authenticates with Steam<br/>Steam redirects back to /auth/callback with assertion"]
    C --> D["3. Gateway verifies Steam OpenID assertion<br/>Extracts Steam64 ID from claimed_id URL"]
    D --> E{"4. Gateway looks up AdminRole table"}
    E -->|"No row found"| F["Reject with 'Not an admin'"]
    E -->|"Row exists"| G["5. Gateway calls panel_register_session reducer<br/>Creates PanelSession row in STDB"]
    G --> H["6. Gateway issues JWT cookie<br/>qs-session: HttpOnly, 8-hour expiry, HS256 signed<br/>Payload: playerId, displayName, adminLevel, iat, exp"]
    H --> I["7. Browser redirected to dashboard<br/>useAuth reads session via GET /auth/me"]

Session Management

Property Value
Cookie name qs-session
HttpOnly Yes (not accessible to JavaScript)
Expiry 8 hours
Algorithm HS256 (via jose library)
Secret JWT_SECRET env var (64+ character random string)

Session Lifecycle

  • Login — Steam OpenID → JWT cookie → PanelSession row created
  • ActiveGET /auth/me returns session data on each page load
  • Mutations — admin actions sent via POST /api/* with cookie attached
  • LogoutPOST /auth/logout → cookie cleared → panel_end_session reducer called
  • Expiry — after 8 hours, GET /auth/me returns 401 → redirect to login

Admin Role Levels

Level Value Permissions
Moderator 0 Ban, kick, whitelist management, audit log view
Admin 1 All Moderator permissions + server config, role management
Owner 2 All Admin permissions + SQL console, table browser, developer tools

First-Time Bootstrap

The server starts with zero admin roles. Bootstrap the first Owner via CLI:

# Using the bootstrap script (recommended):
./panel/bootstrap-admin.sh 76561198XXXXXXXXX YourName

# Or manually:
spacetime sql qs-bridge \
  "INSERT INTO admin_role (player_id, level, granted_by, granted_at) \
   VALUES ('Steam:76561198XXXXXXXXX', 2, 'bootstrap', 0)"

You need level = 2 (Owner) for the first account so you can grant roles to others from the panel.

Inviting Other Admins

Once logged in as Owner:

  • From the panel — Admin Controls → Roles tab → Grant Admin section
  • From CLI:
    spacetime sql qs-bridge \
      "INSERT INTO admin_role (player_id, level, granted_by, granted_at) \
       VALUES ('Steam:THEIR_STEAM64_ID', 1, 'Steam:YOUR_STEAM64_ID', 0)"
    

The new admin can then sign in via Steam — no tokens or secrets needed.

React Hooks

useAuth()

Context consumer for authentication state:

const { user, isLoading, isAuthenticated, logout } = useAuth();
// user: { playerId, displayName, adminLevel } | null

useApi()

Fetch wrapper for gateway API calls (attaches JWT cookie automatically):

const api = useApi();
await api.post('/api/ban', { playerId, reason, duration });

Security Properties

Property Implementation
No client-side secrets JWT is HttpOnly — JavaScript cannot read it
No STDB credentials in browser WebSocket is read-only subscriptions only
Gateway-only mutations All admin reducers verify is_gateway()
No password database Steam authenticates users; STDB stores only Steam IDs
Audit trail Every admin action logged in PanelAuditLog

Troubleshooting

Symptom Cause Fix
"Sign in with Steam" does nothing Gateway not running Start panel/api with npm run dev
Steam login redirects but panel shows error GATEWAY_URL mismatch Set GATEWAY_URL to your panel's public URL
"Not an admin" after Steam login No admin_role row Bootstrap via ./panel/bootstrap-admin.sh
Dashboard shows but no live data STDB connection failed Check VITE_STDB_HOST env var
Session expired 8-hour JWT expiry Sign in again