arizuko

arizukocomponents › authd

authd

What it is

In plain terms, authd is the ID office. It checks who you are — through GitHub, Google, Discord, or Telegram — and hands back a signed token that proves it. Every other daemon trusts that token.

authd is the auth plane: the sole platform-token signer and JWKS publisher. It holds the active ES256 (P-256) signing key in memory, mints every access and refresh token, and acts as the OAuth provider. Backends never sign — they fetch authd’s public JWKS and verify tokens locally through the auth/ library. authd owns auth.db and migrates it itself.

One line to keep straight: authd authenticates, routd authorizes. authd answers “who are you, and what is the ceiling of what you could be allowed”; it does not decide whether a given action on a given folder is permitted. That gate lives in routd, the ACL owner.

Why it exists

If every daemon signed its own tokens, every daemon would hold a signing key, and a leak anywhere would be a leak everywhere. Concentrating the key in one daemon means there is exactly one secret to protect and one place to rotate it. authd is that daemon.

It does the login dance: a user signs in with GitHub, Google, Discord, or the Telegram widget, and authd exchanges that successful login for a short-lived ES256 access token plus a refresh token. The access token’s scope is bounded by a snapshot of the subject’s grants — which authd fetches from routd at mint time, because routd owns the ACL. Refresh tokens rotate with reuse detection: replay a spent token and the whole family is revoked.

It also bootstraps the daemons themselves. At boot authd self-mints service:authd, and it exchanges each daemon’s service key for a service:<name> token — so routd, runed, webd, and proxyd all carry an identity authd can verify. Key rotation keeps one active key (enforced by a partial-unique index) while still serving recently-retired keys until their window closes; RevokeAllNow is the emergency cut.

How it fits

browser login (GitHub / Google / Discord / Telegram)
        |  GET /auth/*
        v
      authd   snapshot grants  <-- routd (GRANTS_URL, the ACL owner)
        |     mint ES256 access + refresh token
        v
      user / daemon holds a token

      every backend (routd / runed / webd / proxyd / dashd / onbod)
        |  GET /v1/keys  (public JWKS, fetched once, cached)
        v
      auth.VerifyToken   verifies offline, no call back to authd per request

Inputs: OAuth callbacks on /auth/*; mint and downscope requests on POST /v1/tokens; daemon service-key exchange on POST /v1/service-token; refresh rotation on POST /v1/refresh; the grants ceiling fetched from routd. Outputs: ES256 tokens; the public JWKS at GET /v1/keys that every backend reads to verify offline.

Hard deps: its own tables, which nothing else reaches. Soft dep: routd via GRANTS_URL for the login-time scope ceiling — unset, sessions come back empty-scope. authd is not the grants authority; it reads the ceiling, it does not set it.

Verify offline, mint in one place

The split is deliberate: minting is centralized (one key, one daemon), verification is distributed (every backend, no round-trip). A request carrying a token never calls back to authd — the backend already has the JWKS and checks the signature locally. authd can be momentarily down and existing tokens still verify; only new logins and refreshes block.

Standalone usage

authd is the most standalone of the split daemons — it owns its DB and its signing key, and it needs no other daemon to mint and verify tokens. The one soft tie is GRANTS_URL: without routd, it still issues tokens, but their scope ceiling is empty.

cd /srv/data/myinstance
export DATA_DIR=/srv/data/myinstance        # resolves <dir>/store/auth.db
export LISTEN_ADDR=:8080
export AUTHD_SERVICE_KEY=$(grep ^AUTHD_SERVICE_KEY .env | cut -d= -f2)
export AUTHD_SERVICE_KEYS='routd=...,runed=...,webd=...,proxyd=...'
export GRANTS_URL=http://routd:8080
# OAuth provider config, mount /auth/* only when present:
export GITHUB_CLIENT_ID=... GITHUB_CLIENT_SECRET=...
export AUTH_BASE_URL=https://example.com
./authd

The /auth/* routes mount only when provider config is present. GET /health returns 200 once the signer is live with at least one serving key.

Looking inside: keys and sessions

Two questions come up the moment something goes wrong, and for a long time neither had an answer short of opening the database by hand: which key is signing right now, and who is logged in.

authd answers both, read-only, and ends a session on request:

Note: ending a session stops the renewal, not the current pass. The browser can no longer get a new one and drops back to the login screen, but a page already open keeps working for up to fifteen minutes — the lifetime of one pass — and then stops. That is the whole revocation model: short lifetimes instead of a revocation list every backend has to consult.

Reading the list and ending a session are separate permissions, so a dashboard that shows you who is logged in does not thereby gain the ability to log them out. Neither permission is one a person can hold: they are resource:verb permissions, and a human’s permissions are folder paths — even the all-folders ** does not match one. The only holder is the dashboard daemon, and the page it serves is operator-gated, so this is enforced twice by two different mechanisms.

That page is /dash/authd/ (see components/dashd). It renders both tables and puts a sign-out button on each live login.

Signing everyone out at once is a different operation, and there is no button for it. It means retiring the active signing key, which invalidates every pass in every browser within the hour. arizuko keeps that out-of-band — you do it by restarting authd with a fresh key — so a whole-fleet logout cannot happen on a mis-click.

What authd does not do

authd does not authorize. It mints and verifies, runs the OAuth dance, and rotates keys — but it never decides whether a caller may touch a particular folder. That decision is routd’s, every time.

Go deeper