Rust workspace serving a Google/Microsoft-Authenticator-style web UI, with
each account's rotating code mirrored into Discord as a message that
rewrites itself in place as the code rolls.
The live mechanism is webhook message editing: POST ?wait=true returns the
message id, PATCH .../messages/{id} rewrites it thereafter. Two approaches
were evaluated and rejected — an externally-hosted image URL is cached by
Discord's media proxy and, worse, must be anonymously reachable, which would
publish live codes on an unauthenticated endpoint; and an animated countdown
drifts out of phase because playback starts at decode time, not on a wall
clock. The countdown instead uses Discord's <t:unix:R> markup, which the
client re-renders every second for free.
Codes are also sent as text in a fenced block so they stay copyable, with
the grouped, readable form on a rendered WebP tile.
- core: RFC 6238 TOTP over HMAC, lenient otpauth:// parsing (issuer conflicts
and non-canonical base32 both occur in the wild), XChaCha20-Poly1305 vault,
Argon2id admin auth
- discord: webhook client with a serialising rate limiter (~2 req/s) so
globally-aligned rollovers don't burst, and error classification that
distinguishes a deleted message (repost) from a dead webhook (stop)
- render: SVG -> WebP tiles via resvg with embedded JetBrains Mono
- http-api: axum, session auth, QR decode with a rescale/contrast/rotation
retry ladder
- webui: offline vanilla JS, Beautiful UI design language, light and dark
Ships as a 27 MB Alpine image (about 8 MB to pull); the binary is fully static musl with no
OpenSSL and no C dependencies, cross-compiled via cargo-zigbuild so building
linux/amd64 on Apple Silicon needs no QEMU.
101 tests, including the RFC 6238 vectors, QR round-trips, and the live push
cycle exercised against a mock Discord.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
35 lines
1.2 KiB
YAML
35 lines
1.2 KiB
YAML
# Discord Authenticator — single-container deployment.
|
|
#
|
|
# docker compose up -d
|
|
#
|
|
# Then open http://<this-host>:8710/ and complete first-run setup.
|
|
#
|
|
# Unlike a PXE server this needs no special networking: it makes outbound
|
|
# HTTPS calls to Discord and serves one HTTP port. Bridge mode is fine.
|
|
|
|
services:
|
|
discord-authenticator:
|
|
image: gitea.milesward.dev/mward4/discord-authenticator:latest
|
|
container_name: discord-authenticator
|
|
restart: unless-stopped
|
|
ports:
|
|
- "8710:8710"
|
|
environment:
|
|
DA_HTTP_ADDR: "0.0.0.0:8710"
|
|
DA_LOG: "info,da=info"
|
|
# Optional. Supply a base64 32-byte key to keep the vault key OFF the
|
|
# data volume — then a leaked copy of the volume is useless on its own.
|
|
# Generate one with: openssl rand -base64 32
|
|
# If unset, a key is generated into the volume as `vault.key` (0600).
|
|
# DA_MASTER_KEY: "…"
|
|
volumes:
|
|
# Vault, key, admin account and settings. Back this up — losing it
|
|
# loses every stored TOTP secret.
|
|
- ./data/work:/var/lib/discord-authenticator
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8710/healthz"]
|
|
interval: 30s
|
|
timeout: 3s
|
|
retries: 3
|
|
start_period: 5s
|