Auth & join tokens
tabbify-auth is the single signing authority for your office. It issues
Ed25519-signed JWTs in two flavors — user-auth tokens for login and
node-join tokens for mesh peers — and every other service validates against
it. It matters because join tokens are what decide who gets onto your
private mesh, and the token claims — not the peer — are what
the coordinator trusts.
Two token kinds
A user-auth token is minted on login (kind: auth). Its claims are sub,
iat, exp, jti; default TTL is 24h. It carries no network or tags.
A node-join token is minted for a peer joining the mesh (kind: join). It
carries network (the mesh network name) and tags — both authoritative, so
a node cannot self-assert what it is allowed to be. Default TTL is 1h.
Both are signed with one Ed25519 key. The public key is served as JWKS at
GET /v1/jwks; the kid in each JWT header points at the right key.
Issuing a join token
Join tokens are admin-only. You pass the network and the tags the peer is permitted to claim, and the auth service bakes them into the JWT:
curl -X POST http://127.0.0.1:8080/v1/tokens/join \
-H 'Authorization: Bearer $AUTH_ADMIN_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"network":"alice","tags":["tag:user-alice"],"ttl":3600,"subject":"alice-laptop"}'
# {"token":"eyJ0eXAiOiJKV1Q...","jti":"019e5ac6-...","kind":"join","expires_at":1779643046}
AUTH_ADMIN_TOKEN guards every admin endpoint (register, join, revoke) and is
compared in constant time. Bootstrap-only for now — there is no self-serve issuance.
Joining the mesh
The peer presents the token as Authorization: Bearer <JWT> when it registers.
Hand it to the joiner via MESH_JOIN_TOKEN:
tabbify-mesh join --name supervisor-01 \
--coordinator https://3.124.69.92:8888 \
--tls-cert ./peer.crt --tls-key ./peer.key --tls-ca ./ca.crt \
--join-token $MESH_JOIN_TOKEN
When the coordinator is started with AUTH_URL set, it calls back to validate
every join, then reads network and tags from the token claims, not the
request. This is the deliberate fix for ACL spoofing: a node's own --tag
flags are ignored once a token is in play.
curl -X POST http://127.0.0.1:8080/v1/validate \
-H 'Content-Type: application/json' -d '{"token":"eyJ0eXAiOiJKV1Q..."}'
# {"valid":true,"subject":"alice-laptop","network":"alice","tags":["tag:user-alice"],"kind":"join","exp":1779643046}
/v1/validate always returns 200 — even for garbage. Validation is a result
(valid: true|false), not an error, and it checks signature, exp (no leeway),
and revocation.
Revocation
Every issued token's jti is recorded. Revoking is instant:
curl -X DELETE http://127.0.0.1:8080/v1/tokens/019e5ac6-ec8d-7b32-bfd0-205066b12dc2 \
-H 'Authorization: Bearer $AUTH_ADMIN_TOKEN'
# {"jti":"019e5ac6-...","revoked":true} → next /v1/validate returns {"valid":false}
Revocation is enforced at /v1/validate time. Services that verify locally via
JWKS accept eventual consistency until they re-check or the (short) TTL expires.
Honest about scope
This is P1. Mesh coordinator integration is live — it validates join tokens on
register today. Without AUTH_URL set on the coordinator (the dev / E1 escape
hatch), there is no validation and --tag flags are trusted from the request.
Local JWKS validation in hot-path services, key rotation, and self-serve
registration are deferred. See self-hosting a node to wire a
peer end to end.