Files
Discord-Authenticator/deploy/unraid/discord-authenticator.xml
T
Miles WardandClaude Opus 4.8 f33effe20a v0.1: self-hosted TOTP authenticator with live Discord delivery
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]>
2026-08-28 16:06:08 -04:00

74 lines
3.8 KiB
XML

<?xml version="1.0"?>
<!--
Unraid Docker template for Discord Authenticator.
Drop this into /boot/config/plugins/dockerMan/templates-user/ on the Unraid
box, or add the repo as a template repository under Docker → Add Container.
Unlike OpenPXE this needs no host networking — it only makes outbound HTTPS
calls to Discord and serves one HTTP port, so bridge mode is correct.
Web UI: http://<unraid-ip>:8710/
-->
<Container version="2">
<Name>Discord-Authenticator</Name>
<Repository>gitea.milesward.dev/mward4/discord-authenticator:latest</Repository>
<Registry>https://gitea.milesward.dev/mward4/-/packages/container/discord-authenticator</Registry>
<Network>bridge</Network>
<MyIP/>
<Shell>sh</Shell>
<Privileged>false</Privileged>
<Support>https://gitea.milesward.dev/mward4/Discord-Authenticator/issues</Support>
<Project>https://gitea.milesward.dev/mward4/Discord-Authenticator</Project>
<Overview>
Self-hosted TOTP authenticator with live Discord delivery.
Add accounts by uploading a QR code, scanning with the camera, or pasting
an otpauth:// link. The web UI lists every account with its rotating code,
the way Google or Microsoft Authenticator does.
The distinctive part: each account keeps ONE message in your Discord
channel, delivered through an incoming webhook and rewritten in place
every time the code rolls. Three accounts means three live tiles that
keep themselves current. The code is posted both as a rendered WebP tile
and as copyable text, and the countdown uses Discord's own relative
timestamps so it ticks in the client without any extra traffic.
SECURITY NOTE: anyone who can read that Discord channel can read these
codes, which is most of the point of a second factor. Use a private
channel in a server only you are in, and treat the webhook URL as a
secret. TOTP secrets are encrypted at rest with XChaCha20-Poly1305.
</Overview>
<Category>Tools:Utilities Security:</Category>
<WebUI>http://[IP]:[PORT:8710]/</WebUI>
<TemplateURL/>
<Icon>https://gitea.milesward.dev/mward4/Discord-Authenticator/raw/branch/main/crates/webui/src/logo.svg</Icon>
<ExtraParams/>
<PostArgs/>
<CPUset/>
<DateInstalled/>
<DonateText/>
<DonateLink/>
<Requires>
A Discord incoming-webhook URL, from Server Settings → Integrations →
Webhooks → New Webhook → Copy Webhook URL. You can add it during first-run
setup or later in Settings.
</Requires>
<Config Name="WebUI Port" Target="8710" Default="8710" Mode="tcp"
Description="Port for the web UI and API."
Type="Port" Display="always" Required="true" Mask="false">8710</Config>
<Config Name="Data" Target="/var/lib/discord-authenticator"
Default="/mnt/user/appdata/discord-authenticator"
Mode="rw"
Description="Vault, encryption key, admin account and settings. BACK THIS UP — losing it loses every stored TOTP secret."
Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/discord-authenticator</Config>
<Config Name="Master key (optional)" Target="DA_MASTER_KEY" Default=""
Mode=""
Description="Base64 of 32 random bytes. Set this to keep the vault key OFF the appdata share, so a leaked backup of the share is useless on its own. Generate with: openssl rand -base64 32. Leave blank to auto-generate a key file inside the data folder instead. WARNING: if you set this later, existing secrets become unreadable — export them first."
Type="Variable" Display="advanced" Required="false" Mask="true"></Config>
<Config Name="Log filter" Target="DA_LOG" Default="info,da=info"
Mode=""
Description="tracing-subscriber EnvFilter expression. Use info,da=debug for more detail."
Type="Variable" Display="advanced" Required="false" Mask="false">info,da=info</Config>
</Container>