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,103 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-and-publish-unraid.sh — build the Discord Authenticator image and push
|
||||
# it to the local Gitea container registry, ready to pull from an Unraid
|
||||
# template.
|
||||
#
|
||||
# Run this on any host that can reach Gitea and has Docker. It clones a fresh
|
||||
# copy rather than building the working tree, so what ships is exactly what is
|
||||
# committed.
|
||||
#
|
||||
# Inputs (env vars, all optional except GITEA_TOKEN):
|
||||
# GITEA_TOKEN personal access token with `write:package` scope
|
||||
# GITEA_HOST default: gitea.milesward.dev
|
||||
# GITEA_OWNER default: mward4
|
||||
# GITEA_REPO default: Discord-Authenticator
|
||||
# IMAGE_NAME default: discord-authenticator
|
||||
# IMAGE_TAG default: 0.1.0 (also pushed as :latest)
|
||||
# PLATFORM default: linux/amd64 (Unraid is x86_64)
|
||||
# GIT_REF default: v0.1 (tag/branch to build)
|
||||
# WORKDIR default: /tmp/discord-authenticator-build (removed on exit)
|
||||
#
|
||||
# After this, set the Unraid template's Repository to:
|
||||
# <GITEA_HOST>/<owner>/discord-authenticator:<tag> (or :latest)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GITEA_HOST=${GITEA_HOST:-gitea.milesward.dev}
|
||||
GITEA_OWNER=${GITEA_OWNER:-mward4}
|
||||
GITEA_REPO=${GITEA_REPO:-Discord-Authenticator}
|
||||
IMAGE_NAME=${IMAGE_NAME:-discord-authenticator}
|
||||
IMAGE_TAG=${IMAGE_TAG:-0.1.0}
|
||||
PLATFORM=${PLATFORM:-linux/amd64}
|
||||
GIT_REF=${GIT_REF:-v0.1}
|
||||
WORKDIR=${WORKDIR:-/tmp/discord-authenticator-build}
|
||||
|
||||
# OCI distribution rejects uppercase repository paths, so the image name is
|
||||
# lowercased independently of the repo name.
|
||||
IMAGE_NAME="$(printf '%s' "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]')"
|
||||
FULL_TAG="$GITEA_HOST/$GITEA_OWNER/$IMAGE_NAME:$IMAGE_TAG"
|
||||
LATEST_TAG="$GITEA_HOST/$GITEA_OWNER/$IMAGE_NAME:latest"
|
||||
|
||||
if [[ -z "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "ERROR: set GITEA_TOKEN to a token with write:package scope." >&2
|
||||
echo " Gitea -> Settings -> Applications -> Generate New Token." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for tool in git docker; do
|
||||
command -v "$tool" >/dev/null 2>&1 \
|
||||
|| { echo "ERROR: '$tool' not found in PATH" >&2; exit 2; }
|
||||
done
|
||||
|
||||
# A private docker config so the token never lands in ~/.docker/config.json.
|
||||
# The trap wipes it (and the checkout) on any exit path.
|
||||
DOCKER_CONFIG="$(mktemp -d)"
|
||||
export DOCKER_CONFIG
|
||||
trap 'rm -rf "$DOCKER_CONFIG" "$WORKDIR"' EXIT
|
||||
|
||||
echo
|
||||
echo "==> Workspace : $WORKDIR"
|
||||
echo "==> Gitea host : $GITEA_HOST"
|
||||
echo "==> Building ref : $GIT_REF"
|
||||
echo "==> Image to push : $FULL_TAG"
|
||||
echo "==> Platform : $PLATFORM"
|
||||
echo
|
||||
|
||||
# Step 1: shallow clone of the exact ref.
|
||||
rm -rf "$WORKDIR"
|
||||
git -c "http.extraHeader=Authorization: token $GITEA_TOKEN" \
|
||||
clone --depth 1 --branch "$GIT_REF" \
|
||||
"https://$GITEA_HOST/$GITEA_OWNER/$GITEA_REPO.git" "$WORKDIR"
|
||||
|
||||
cd "$WORKDIR"
|
||||
|
||||
# Step 2: build. --pull so a stale cached base image doesn't bake in a CVE
|
||||
# that upstream has already fixed.
|
||||
#
|
||||
# The Dockerfile cross-compiles from the builder's native architecture, so
|
||||
# this is fast even when building linux/amd64 on an arm64 host — no QEMU.
|
||||
docker build \
|
||||
--pull \
|
||||
--platform "$PLATFORM" \
|
||||
--file deploy/docker/Dockerfile \
|
||||
--tag "$FULL_TAG" \
|
||||
--tag "$LATEST_TAG" \
|
||||
.
|
||||
|
||||
# Step 3: log in. --password-stdin keeps the token out of `ps` and shell history.
|
||||
printf '%s' "$GITEA_TOKEN" | docker login \
|
||||
--username "$GITEA_OWNER" \
|
||||
--password-stdin \
|
||||
"$GITEA_HOST"
|
||||
|
||||
# Step 4: push both tags.
|
||||
docker push "$FULL_TAG"
|
||||
docker push "$LATEST_TAG"
|
||||
|
||||
docker logout "$GITEA_HOST" || true
|
||||
|
||||
echo
|
||||
echo "==> Done. Pull from an Unraid template with:"
|
||||
echo " $FULL_TAG"
|
||||
echo " $LATEST_TAG"
|
||||
echo
|
||||
Reference in new issue
Block a user