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]>
This commit is contained in:
commit
f33effe20a
46 files changed
+11160
No files matched your search
@@ -0,0 +1,73 @@
|
||||
<?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>
|
||||
Reference in new issue
Block a user