auth
Auth answers who is calling. authd, the only token authority, turns a provider login into one canonical user id and signs a JWT that carries it. Every later permission check reads that id.
Each provider login gives a sub
You can log in with any provider. Each provider returns a string that the auth code calls a sub, short for “subject”: the id of this user at this provider. A sub always starts with its provider:
github:48291744
google:114019583462810573829
discord:837412095138652180
One person usually has several subs. The first sub you log in with becomes the canonical sub of your account, and it never changes. Every grant row keys on it, so a canonical sub that changed would remove all your rights without a warning. To add another provider, click “Link…” at /dash/profile/ and complete the OAuth flow of that provider. authd records the new login as one more identity of the same account. After that, a login with any linked provider gives a token with the canonical sub. Each provider runs OAuth2 with a signed CSRF state and PKCE. reference / env lists the optional allow-lists for each provider.
The browser holds two tokens
After login, the browser holds two things:
- An access JWT
- Valid for 15 minutes and kept in
localStorage.authdsigns it with ES256. It carries the canonicalsub, thetyp, a snapshot of the scopes at login and an expiry. The browser sends it asAuthorization: Bearer …on API calls. Any daemon can verify it offline against the JWKS ofauthd. - A refresh token
- 32 random bytes, stored on the server as a SHA-256 hash. The browser keeps it in an
HttpOnly; SameSite=Lax; Path=/cookie,Secureon HTTPS, valid for 30 days. Each refresh replaces the cookie, and each cookie works once, so a stolen cookie gives one exchange and then stops working.
authd resolves the canonical sub once
authd decides the canonical sub exactly once, just before it creates the JWT. Whatever provider you used, the token carries your canonical sub, and auth.Authorize resolves nothing itself: the sub it sees is the answer. authd writes the provider you used to its audit log as a login event. It never writes the provider into the token, because a claim would disappear on the first refresh.
A dashboard request passes proxyd before it reaches dashd
You click a button in the operator dashboard. The browser sends a request to https://your-instance/dash/groups. The request takes these steps:
- The request reaches
proxydon the portPROXYD_LISTENsets, 8080 by default.proxydserves plain HTTP, so a proxy in front of it ends HTTPS.proxydlooks for theAuthorization: Bearerheader, or the refresh-token cookie on web pages. proxyd.tryAuthverifies the JWT: an ES256 signature, checked offline against the JWKS ofauthd. If the JWT is valid,proxydreadssub,nameand thegroupsof the user. A plain/auth/*login request instead gets a 302 redirect toauthd, which owns the OAuth flow.proxyd.setUserHeaderscopies the request, removes any identity headers from the client and sets three:X-User-Sub,X-User-NameandX-User-Groupsas JSON. It then attaches an ES256 bearer fromauthdwhose subject isservice:proxyd. This bearer is the transit proof.proxydsends the copied request todashdover the docker network.dashdwraps every/dash/*handler in the middleware fromauth/middleware.go. The middleware callsauth.ProxydTransit. It verifies the signature, expiry and issuer of the bearer against the JWKS ofauthd, and checks that the subject isservice:proxyd. Only then does it trust the three headers. If the bearer is missing or wrong, the middleware removes the headers and redirects to/auth/login.- The handler reads
r.Header.Get("X-User-Sub")and trusts it.auth.Authorizeuses that sub to decide what the user can do.
authd holds the only ES256 private key. It signs the browser JWT and the service:proxyd transit bearer, and both verify against its public JWKS. AUTH_SECRET is an HMAC key for the OAuth state cookie and verifies no bearer. The X-User-* headers carry no signature, so the transit bearer makes them trustworthy.A refresh token used twice ends the whole login
A refresh token works once. When you present it, authd uses it up and returns a successor. authd keeps the used row, marks it and never deletes it. That mark is how authd detects reuse. If the same token arrives a second time, one of the two holders is not you, and authd cannot tell which. So it revokes the whole chain, every token that came from that login, and logs out both holders.
authd uses up the token and creates its successor in one database write. With two writes, a revoke from the second holder could land between them. It would revoke every token that existed at that moment, and then the successor would appear unrevoked, valid for another thirty days. With one write, no such gap exists.
Logout works in the same way. A logout revokes the chain, and a refresh already in progress cannot get ahead of it, because the exchange step refuses a revoked chain. It does not only check whether the token itself was used.
An operator can list sessions and end one
An operator can ask authd two questions over HTTP, with no database shell:
- Which key signs?
GET /v1/signing_keyslists each key with its id, creation time and retirement time. It also shows when a retired key stops verifying the tokens it signed. The public JWKS at/v1/keysholds only what a verifier needs./v1/signing_keysanswers whether a key rotation took effect. It returns metadata only, and no argument can reach a private key through it.- Who is logged in?
GET /v1/sessionslists one row per login: the account, the granted scope, the start time and the number of rotations. It also shows whether the login is active, revoked or expired. Token values never appear.authdstores only a hash and does not serve the hash either.
DELETE /v1/sessions/{family_id} ends one session. Use it when a session must end and its holder cannot end it, for example after a lost laptop or when a person leaves. authd writes the revoke to its audit log in the same transaction as the revoke. So the revoke appears in the audit trail with everything else.
Reading the list and ending a session are separate permissions. So a dashboard that shows who is logged in cannot log anyone out because of that. Both permissions are operator-level, and no ordinary user token can reach them, whatever its grants. The permissions of a user are folder paths, and a folder path can never satisfy a permission of this shape.
In a browser, the page is /dash/authd/: both tables, and a sign-out button on each live login. A sign-out stops the renewal. A page that the person already has open keeps working until its token expires, after about fifteen minutes, and then stops. To sign everyone out at once, you must retire the active signing key. No endpoint, CLI command or button does that.
A login that another account owns cannot be linked
You are logged in as google:114alice and click “Link GitHub”. The callback gets github:48291744. If that GitHub login is new, it joins your account. If it already belongs to you, nothing changes. If it belongs to another account, the link fails. authd never merges accounts, and it never links two logins because they share a verified email. Each of these would let one person take over the account of another. No unlink exists either.
authd/README.md has the full detail
[authd/README.md] describes the keys, the mint modes, the login snapshot and the cookie flags in full. grants explains what your canonical sub can do when it reaches a backend.