Skip to content

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:

  1. 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.
  2. 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, or qumo auth token while testing.
  3. The client connects with the token in the relay URL: https://relay:4433/?jwt=<token> (WebTransport), or moqt://relay:4433/?jwt=<token> (native QUIC).
  4. 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]
CommandDescription
keygenGenerate a signing key pair: the private key, and the public key set the relay trusts.
tokenSign a token by hand, for testing.

keygen

qumo auth keygen [-prefix acme/app] [-out signing-key.jwk] [-keys keys.json]
FlagDefaultDescription
-prefix(none)Confine the key: every path a token signed with it may grant must lie at or beneath this prefix.
-outsigning-key.jwkThe private signing key (an Ed25519 JWK). Keep it on your app’s server.
-keyskeys.jsonThe 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’s GET), post (sending one message into a track a funnel gathers many senders into and publishes; a relay never reads it as publishing) and redact (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 (not a/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)