Open-source web implementation of Steam Desktop Authenticator (SDA) with multi-user and multi-account support.
Russian version: readme_ru.md
- Backend: Fastify + TypeScript (REST API + WebSocket notifications)
- Frontend: React + Vite + Tailwind + PWA
- DB: MySQL 8 in Docker with encrypted secrets/session data
- Shared infra: Redis for rate limiting in Docker deployments
- Bot: Aiogram (single Telegram bot for all users)
- License: MIT
git clone git@github.com:feskolech/steamwebauthenticator.git
cd steamwebauthenticator
cp .env.example .envEdit .env before starting:
- set
NODE_ENV=production - set
APP_URLto your public HTTPS origin, for examplehttps://steam.example.com - set
API_URLto the same origin API path, for examplehttps://steam.example.com/api - replace
ADMIN_EMAILandADMIN_PASSWORD - generate strong
JWT_SECRET,COOKIE_SECRET, andENCRYPTION_KEY - set MySQL passwords
- optionally set Telegram bot and Turnstile keys
- when serving frontend/API from the same reverse-proxy origin, leave
VITE_API_URLandVITE_WS_URLempty - if Turnstile is enabled, set
VITE_TURNSTILE_SITE_KEYto the same value asTURNSTILE_SITE_KEY
Generate strong secrets:
openssl rand -hex 32
openssl rand -hex 32
openssl rand -hex 32Start production containers:
make deployOr use Docker Compose directly:
docker compose up -d --buildProduction containers bind to localhost by default:
- Frontend:
127.0.0.1:3100 - Backend API + WebSocket:
127.0.0.1:3101
Put your external Nginx/Caddy/Traefik reverse proxy in front of these ports:
/->http://127.0.0.1:3100/api->http://127.0.0.1:3101/ws->http://127.0.0.1:3101
Recommended production .env shape behind a reverse proxy:
NODE_ENV=production
APP_URL=https://steam.example.com
API_URL=https://steam.example.com/api
VITE_API_URL=
VITE_WS_URL=
FORCE_HTTPS=trueUse explicit frontend URLs only when the browser really talks to a separate API/WS origin:
VITE_API_URL=https://api.steam.example.com
VITE_WS_URL=wss://api.steam.example.com/wscp .env.example .env
make devOpen:
- Frontend:
http://localhost:3000 - Backend API:
http://localhost:3001 - OpenAPI:
http://localhost:3001/api-docs/openapi.json(enabled by default in dev/test)
- Fastify: lower overhead and better throughput for polling/real-time workloads.
- TypeScript: safer refactoring and shared API contracts.
- MySQL 8 + Docker internal network: relational consistency, easy VPS deploy, DB isolated from public access.
- Redis-backed rate limiting: shared throttling across backend instances and restarts, with in-memory fallback outside Redis setups.
- React + Vite + Tailwind: fast DX, responsive UI, simple theming.
- Vite PWA plugin: installable app + service worker support.
- Aiogram bot: mature async Telegram framework for command and deep-link flows.
.maFileimport/export with AES-256-GCM encryption in DB.- Multi-user + unlimited Steam accounts per user.
- Steam code generation from
shared_secret. - Trade/login confirmations queue with manual confirm/reject.
- Auto-confirm toggles with per-account delay.
- Account folders and tags with filtering in the accounts view.
- Telegram OAuth-like login via bot deep-link.
- Telegram account linking with
/add=<code>. - Telegram commands:
/accounts,/codes,/confirm <trade_id>,/qr <Steam QR link>,/status. - Passkey/WebAuthn login, including usernameless passkey login when the device supports discoverable credentials.
- TOTP authenticator app support.
- Recovery codes for break-glass account recovery.
- JWT cookie sessions, CSRF protection, Helmet, rate limiting, optional Turnstile.
- Registration policies: open, disabled, domain allowlist, invite-only.
- Admin panel with registration controls, invite codes, user deletion, and audit trail.
- Webhook notifications for login/trade/session-expired events with generic and Discord targets.
- i18n EN/RU + light/dark theme.
- OpenAPI JSON at
/api-docs/openapi.jsonwhenOPENAPI_ENABLED=true.
- Email:
admin@admin.com - Password:
admin123
Override with .env: ADMIN_EMAIL, ADMIN_PASSWORD
Important:
- The default admin password is for local bootstrap only.
- Production startup is blocked if
ADMIN_PASSWORD=admin123. - Production startup is also blocked if
JWT_SECRET,COOKIE_SECRET, orENCRYPTION_KEYstill use placeholder-style values such aschange_me..., are shorter than 32 characters, or reuse the same secret value.
- Monolith API-first backend (
/api/...) + WS (/ws) on port3001. - Frontend SPA on port
3000. - Frontend server (Vite/Nginx) proxies
/apiand/wsto backend. - MySQL and Redis are isolated in internal Docker network and not exposed externally.
- Optional reverse proxy service (
nginx) in compose profileproxy.
.
├── backend/ # Fastify API (TypeScript)
├── frontend/ # React + Vite + Tailwind + PWA
├── bot/ # Aiogram Telegram bot
├── docker/
│ ├── mysql/init.sql # Schema bootstrap
│ └── nginx/nginx.conf # Reverse proxy example
├── docker-compose.yml
├── docker-compose.dev.yml
├── Makefile
├── .env.example
├── README.md
└── readme_ru.md
make dev- Docker dev stack with hot reload.make build- build production images.make up- run production stack detached.make deploy- alias formake up.make down- stop and remove containers.make lint- backend + frontend lint.make test- backend Jest + frontend Cypress (via docker service).make logs- follow container logs.
See .env.example.
Core variables:
DB_*MySQL connection and bootstrap user credentials.JWT_SECRET,COOKIE_SECRET,ENCRYPTION_KEYsecurity secrets.APP_URL,API_URLbrowser/backend origins.VITE_API_URL,VITE_WS_URLoptional frontend build-time API/WebSocket origins. Leave empty for same-origin reverse proxy deploys.RATE_LIMIT_REDIS_URL,RATE_LIMIT_REDIS_PREFIXshared rate limiting backend; Docker Compose defaults to internal Redis and aNODE_ENV-based prefix.FORCE_HTTPSenables HTTP->HTTPS redirects in production (trueby default, can be disabled for special deployments).OPENAPI_ENABLEDcontrols/api-docs/openapi.json; enabled by default in dev/test and should stay off in production unless explicitly needed.TELEGRAM_BOT_TOKEN,TELEGRAM_BOT_USERNAMEbot settings.STEAM_POLL_INTERVAL_SECauto-confirm polling interval.TURNSTILE_ENABLED,TURNSTILE_SITE_KEY,TURNSTILE_SECRET_KEYoptional Cloudflare Turnstile backend protection.VITE_TURNSTILE_SITE_KEYfrontend public site key for invisible Turnstile registration flow. It must equalTURNSTILE_SITE_KEY.
Generate strong secrets before production deploy, for example:
openssl rand -hex 32
openssl rand -hex 32
openssl rand -hex 32Then place the generated values into .env as:
JWT_SECRET=<first generated value>
COOKIE_SECRET=<second generated value>
ENCRYPTION_KEY=<third generated value>Important operator notes:
- If
TELEGRAM_BOT_TOKENis empty or starts withchange_me, bot service stays in disabled idle mode. - If you enable
OPENAPI_ENABLEDin production, prefer exposing it only behind admin auth, VPN, or IP allowlisting. APP_URLmust exactly match the browser-facing origin used for WebAuthn/passkeys. If your reverse proxy exposes another hostname or scheme, passkey login/registration will fail.
- MA encryption: AES-256-GCM per user.
- Key derivation: per-user key derived from bcrypt password hash + global
ENCRYPTION_KEY. - Auth: JWT in HTTP-only cookie.
- 2FA: Telegram, passkeys, TOTP authenticator app.
- Break-glass recovery: one-time recovery codes can be generated from settings; using one resets configured 2FA and removes registered passkeys for that user.
- Sensitive actions: password re-confirmation required before
.maFileexport, recovery code reveal, manual Steam session updates, and recovery code regeneration. - CSRF: double-submit protection for mutating endpoints.
- Brute-force/DoS:
rate-limiter-flexiblein auth/write paths. - Shared throttling: Docker deployments use Redis-backed rate limits with in-memory insurance fallback if Redis is temporarily unavailable; current Compose defaults do not persist limiter state across Redis restarts.
- Anti-bot registration: signed registration challenge, honeypot, dedicated registration rate limiter and optional invisible Cloudflare Turnstile.
- Hardening:
helmet, CORS with credentials. - WebSocket auth: cookie/bearer only, query-string auth disabled.
- API caching:
/api/*responses are served withCache-Control: no-store. - DB isolation: MySQL only on internal Docker network (
db_internal).
The admin panel supports these registration modes:
open- normal self-registration.disabled- registration blocked for everyone.domain_allowlist- only email domains from the configured allowlist can register.invite_only- registration requires a valid invite code.
Invite-only mode also includes invite code management in the admin panel.
- In Settings click
Generate /add code. - Send
/add=<code>to the bot within 15 minutes. - The bot binds
telegram_user_idto your web user.
- On login page click
Login via Telegram. - Open the bot manually using the provided button or
/start login_<code>command. - Confirm the login in the bot.
- Return to the web page; it polls and creates the session automatically.
Deploy frontend and backend together when changing Telegram login polling, because the flow now requires the x-telegram-poll-token header instead of the legacy query-string token.
- A person opens a Steam QR login on their device and sends you the QR image or
https://s.team/q/...link. - In the account detail page, paste/upload the QR and re-enter your SteamGuard Web password; or send the image to the linked Telegram bot / use
/qr <link>. - Select one of your own Steam accounts in Telegram. The account approves the QR once and the request expires after two minutes.
The QR itself, mobile token, and .maFile secrets never leave the server. The web flow requires sensitive-action password confirmation; Telegram only offers accounts owned by the linked Telegram user.
- Passkeys can be registered from Settings.
- Login supports both email-first passkey flow and usernameless passkey login.
- Usernameless passkey login depends on discoverable credentials supported by the authenticator/platform.
- TOTP setup is started in Settings.
- The app shows a QR code and manual secret.
- After verifying the first code, TOTP can be selected as the primary 2FA method.
- Recovery codes are generated from Settings after password re-authentication.
- Codes are shown only once.
- Using a recovery code signs the user in and clears configured 2FA/passkeys so the user can recover access safely.
- Accounts can be grouped into folders.
- Accounts can have multiple tags.
- The accounts page supports filtering by folder and tag.
- Account details page supports assigning folder/tags per account.
The admin panel includes an audit view with filters by category and actor.
Main categories:
steamauthsecurity
Examples of tracked events:
- registration policy changes
- invite creation/deletion
- webhook creation/testing/deletion
- TOTP enablement
- recovery code regeneration/use
- admin user deletion
- Steam confirmations and session events
Webhook settings are available in the user Settings page.
Supported targets:
generic- JSON POST webhookdiscord- Discord embed webhook
Supported events:
tradeloginsteam_session_expired
Notes:
- Webhook delivery failures are recorded in DB and do not break the in-app notification flow.
- Test delivery is available from Settings.
Generic webhook payload shape:
{
"event": "trade",
"occurredAt": "2026-03-22T12:00:00.000Z",
"payload": {
"accountId": 1,
"accountAlias": "Main",
"headline": "New trade offer",
"summary": "..."
}
}Steam mobile confirmations require valid session tokens (steamLoginSecure, sessionid, optional oauthToken).
You can set/update them in account detail page (/accounts/:id) or they are imported if present in .maFile session payload.
Only active pending confirmations are shown in the queue view.
Internal bot endpoints are under /api/telegram/bot/* and protected by header:
x-telegram-bot-token: $TELEGRAM_BOT_TOKEN
- Install Docker + Docker Compose before running
make deploy. - Configure
APP_URLandAPI_URLto match your real external HTTPS origin. - The production Compose stack binds frontend/backend to localhost host ports
3100and3101. - If you want anti-bot registration, configure Cloudflare Turnstile keys in
.env.
Optional bundled Nginx proxy:
docker compose --profile proxy up -d- Backend unit/API tests: Jest (
backend/tests/*). - Frontend unit tests: Vitest (
frontend/src/utils/format.test.ts). - E2E smoke: Cypress (
frontend/cypress/e2e/smoke.cy.ts).
For local host runs Cypress may require system dependencies (Xvfb). make test uses cypress/included Docker image to avoid host setup issues.
MIT (LICENSE).






