arizuko

arizukoconcepts › auth

auth

A persona is how the agent sounds; auth is how the platform knows who is actually calling. Before anything can authorize a principal, something has to mint it — and that one job belongs to authd, the sole token authority. authd turns a browser login (GitHub, Google, Discord, Telegram, or a local password) into one canonical user id and an ES256-signed JWT. Every other daemon verifies that token offline against authd’s published JWKS — nobody else signs. The id every later permission check reads is the id this token carries.

why identity at all

arizuko is a bunch of small Go daemons (routd, runed, webd, dashd, proxyd, more) that talk over HTTP. None of them owns the login session by itself. When dashd serves your operator page or routd decides whether to deliver a message, it needs an answer to one question: who is this request from? If each daemon re-implemented login, every one would be a place to get it wrong. Instead exactly one daemon (authd) runs OAuth and mints the token; proxyd sits in front, verifies it, and writes the answer into headers the backends trust.

where identity comes from

You log in with any provider. Each provider gives back a string the auth code calls a sub — short for "subject," but think "this user's id with this provider." subs always carry their provider as a prefix:

github:48291744
google:114019583...
discord:837412...
telegram:user/5511234
local:alice

One human usually has several. You link them at /dash/profile: pick one provider as your canonical sub, click "Link…" on another, finish the OAuth dance, and arizuko writes auth_users.linked_to_sub = <canonical> for the new row. From then on, any linked sub resolves to the same canonical sub on login. Each provider runs standard OAuth2 (Google and Discord add PKCE; Telegram uses its HMAC-verified Login Widget; Local is username + argon2id), with optional allow-list gates per provider — the env flags are in reference / env.

two tokens

After login, the browser holds two things:

one resolve point

The canonical sub is decided exactly once, inside authd: it resolves the canonical sub right before minting the JWT. Whatever provider you logged in with, the minted token carries your canonical sub. Downstream daemons never re-resolve — the sub they see is the answer. Link chains are not allowed; LinkSubToCanonical rejects them, so the lookup is always one hop.

a request, end to end

Suppose you click a button in the operator dashboard. The browser sends an HTTPS request to https://your-instance/dash/groups. Trace it:

  1. The request arrives at proxyd on port 443. It looks for the Authorization: Bearer header (or, on web pages, the refresh-token cookie).
  2. proxyd.tryAuth verifies the JWT — an ES256 signature checked offline against authd’s JWKS. If valid, it extracts sub, name, and the user's groups. A bare /auth/* login request is instead 302’d to authd, which owns the OAuth flow.
  3. proxyd.setUserHeaders clones the request and stamps four headers: X-User-Sub, X-User-Name, X-User-Groups (JSON), and X-User-Sig — an HMAC of the first three using PROXYD_HMAC_SECRET.
  4. The cloned request is proxied to dashd over the docker network.
  5. dashd wraps every /dash/* handler in auth.RequireSigned(PROXYD_HMAC_SECRET) from auth/middleware.go. The middleware recomputes the HMAC and compares to X-User-Sig. Mismatch → strip all four headers, redirect to /auth/login.
  6. The handler reads r.Header.Get("X-User-Sub") and trusts it. That sub is what auth.Authorize uses to decide what the user may do.
Two boundaries, two keys. authd’s ES256 private key signs the JWT the browser holds; every daemon verifies it against the public JWKS. PROXYD_HMAC_SECRET signs the identity header proxyd stamps for backends. Different boundaries: browser ↔ proxyd (JWT) vs proxyd ↔ backend (HMAC header).

collision — logged in as A, OAuth as B

You are logged in as google:114alice and you click "Link GitHub." The callback gets a fresh sub github:48291744. Three cases the code handles cleanly:

No silent ghost-account creation, no ambiguous merges. The user always picks.

where to go next

For the full state machine, OAuth-state cookie layout, and DB schema, read specs/1/f-auth-oauth.md. For what your canonical sub is allowed to do once it lands in a backend, see grants.