auth
qumo auth sets up session auth for a relay (configuration). Your app keeps the only decision that is really yours: who may publish or watch what, which it states by what it signs. The key format, signing, verification and path confinement are done for you.
How it fits together:
- Once: generate a signing key pair (
qumo auth keygen). The private key stays on your app’s server; the relay gets the public one, in a key set. - Per client: your server signs a token naming what that client may do on which broadcasts and tracks (below). Use the Go package
github.com/qumo-dev/qumo/token, orqumo auth tokenwhile testing. - The client connects with the token in the relay URL:
https://relay:4433/?jwt=<token>(WebTransport), ormoqt://relay:4433/?jwt=<token>(native QUIC). - The relay verifies the token itself against the key set (
QUMO_AUTH_KEYS=keys.json qumo relay) and enforces what it grants. The token’s expiry decides whether a session may start; the session then lives on until it closes or its key leaves the set (configuration).
Usage
qumo auth <command> [flags]| Command | Description |
|---|---|
keygen | Generate a signing key pair: the private key, and the public key set the relay trusts. |
token | Sign a token by hand, for testing. |
keygen
qumo auth keygen [-prefix acme/app] [-out signing-key.jwk] [-keys keys.json]| Flag | Default | Description |
|---|---|---|
-prefix | (none) | Confine the key: every path a token signed with it may grant must lie at or beneath this prefix. |
-out | signing-key.jwk | The private signing key (an Ed25519 JWK). Keep it on your app’s server. |
-keys | keys.json | The public key set (a JWK Set) for QUMO_AUTH_KEYS. |
It prints the key’s kid (its RFC 7638 thumbprint), the paths it may grant, and the next steps. It refuses to overwrite an existing file, since replacing a key would invalidate every token signed with it; if it can’t write the key set, it removes the private key it just wrote.
token
qumo auth token [-key signing-key.jwk] [-scope ACTIONS:BROADCAST[:TRACK] ...] [-publish PATH] [-subscribe PATH] [-sub SUBJECT] [-ttl 1h]Signs a token granting the scopes -scope names, valid for -ttl, at most one hour. -scope may be repeated: ACTIONS is a comma-separated list of publish, subscribe, fetch, post and redact; BROADCAST is a path, or a/b/** for it and every path beneath it; TRACK is one track name, or omitted for every track. -publish PATH is short for -scope publish:PATH/**, and -subscribe PATH for -scope subscribe,fetch:PATH/**; they add to any -scope. At least one scope is needed. -sub names the bearer. For example, qumo auth token -sub alice -scope post:room/123:chat -scope fetch:room/123/**. The token alone goes to stdout, so T=$(qumo auth token …) captures it; what it grants and when it expires go to stderr. A path outside the key’s prefix is refused here, as the relay would refuse the token; token.Sign does the same.
What a token grants
A token grants scopes: actions on broadcasts and tracks, the JSON counterpart of the moqt claim of CAT-4-MOQT (draft-ietf-moq-c4m). Each scope lists:
actions:publish(sending a track’s groups through a relay),subscribe(live),fetch(history: a funnel’sGET),post(sending one message into a track a funnel gathers many senders into and publishes; a relay never reads it as publishing) andredact(taking a record out of a funnel’s track; for credentials the app keeps to itself). Any other action refuses the token.broadcast:{"exact": "a/b"}for that path alone, or{"prefix": "a/b"}for it and every path beneath it on/boundaries (nota/bc). Either must lie within the key’s prefix.track(optional):{"exact": "chat"}for that track alone; omitted, every track.
Whatever no scope grants is denied, and a scope member the verifier doesn’t know refuses the token, so a misspelled track never widens a scope to every track. A token may name its bearer in sub, which a funnel records as the sender; without sub it names no one.
{"sub": "alice",
"scopes": [{"actions": ["post"], "broadcast": {"exact": "acme/app/rooms/42"}, "track": {"exact": "chat"}}],
"iat": 1791370000, "nbf": 1791370000, "exp": 1791370600, "jti": "…"}path_auth tokens are accepted too, as other MoQ implementations sign them (the @moq/token convention), and read as the scopes they amount to: pub as a publish scope, and sub as a subscribe and fetch scope, each on its path and every path beneath it, on every track. So
{"path_auth": {"root": "acme/app/rooms/42", "pub": "alice", "sub": ""}, "iat": …, "nbf": …, "exp": …}grants what [{"actions": ["publish"], "broadcast": {"prefix": "acme/app/rooms/42/alice"}}, {"actions": ["subscribe", "fetch"], "broadcast": {"prefix": "acme/app/rooms/42"}}] does. A token carrying both path_auth and scopes is refused, and so is one carrying path_auth and sub. qumo auth token and token.Sign write scopes only.
On the relay, a session may announce a broadcast a publish scope matches on every track, one naming no track, and the relay takes its tracks from it. A broadcast has one publisher, so a publish scope naming one track announces nothing. Give each publisher its own broadcast path; to write one track into a broadcast a funnel publishes, grant post there instead. The relay ignores post. A session may subscribe to a track a subscribe scope matches, by broadcast and track name. The relay ignores fetch, so a credential granting only fetch (a funnel reader’s) may do nothing there.
Signing in your app (Go)
key, err := token.LoadSigningKey("signing-key.jwk") // once, at start
// per client, after your app has decided what this user may do:
tok, err := token.Sign(key, token.Grant{
Subject: "alice",
Scopes: []token.Scope{
// publish every track of the broadcasts at and beneath acme/app/rooms/42/alice
{Actions: []token.Action{token.ActionPublish}, Broadcast: "acme/app/rooms/42/alice", Prefix: true},
// watch and read the history of the whole room
{Actions: []token.Action{token.ActionSubscribe, token.ActionFetch}, Broadcast: "acme/app/rooms/42", Prefix: true},
// post into its chat track at a funnel, as alice
{Actions: []token.Action{token.ActionPost}, Broadcast: "acme/app/rooms/42", Track: "chat"},
},
}, time.Hour)