Authentication
moq-relay uses JWT (JSON Web Tokens) for authentication and authorization. Tokens control who can publish or subscribe to which paths.
Overview
There are two authentication modes:
Single Key (--auth-key)
A single JWK file used to verify all tokens. No kid header is required in JWTs. Good for development and simple deployments.
Key Directory (--auth-key-dir)
For production use with key rotation. Keys are resolved on demand by extracting the kid from the JWT header and fetching the corresponding key file.
- Generate signing keys (a random key ID is assigned automatically)
- Store each key as
{kid}.jwkin a directory or serve via HTTP - Configure the relay with the key directory or URL
- Issue tokens to clients with their allowed paths
- Clients connect with
?jwt=<token>query parameter
Quick Start
Generate a Key
Using the Rust CLI:
# Symmetric key (simpler, key must stay secret)
moq-token-cli generate --out my-key.jwk
# Save to a directory as {kid}.jwk
moq-token-cli generate --out-dir ./keys/
# Asymmetric key (private signs, public verifies)
moq-token-cli generate --algorithm ES256 --out private.jwk --public public.jwk
# Asymmetric key, both saved to directories as {kid}.jwk
moq-token-cli generate --algorithm ES256 --out-dir ./private/ --public-dir ./keys/A random key ID is generated if --id is not specified.
Configure the Relay
Single key (simplest):
[auth]
key = "my-key.jwk"Key directory (for key rotation):
[auth]
# Point to the public keys directory (from --public-dir).
# For asymmetric algorithms, the relay only needs public keys to verify tokens.
key_dir = "/etc/moq/keys/"Remote key server:
[auth]
key_dir = "https://api.example.com/keys"Issue a Token
# Allow publishing to demo/my-stream and subscribing to anything under demo/
moq-token-cli sign --key my-key.jwk --root demo --publish my-stream --subscribe ""The client connects with the token. The connection path can be the root or any parent:
# Connect at the token's root
https://relay.example.com/demo?jwt=eyJhbGciOiJIUzI1NiIs...
# Connect at the server root (permissions still scoped to demo/)
https://relay.example.com/?jwt=eyJhbGciOiJIUzI1NiIs...Key Resolution
Single Key Mode (--auth-key)
The relay uses the specified key file to verify all incoming JWTs. No kid header is required in the token.
Key Directory Mode (--auth-key-dir)
Key files are stored as JSON by default. Legacy base64url-encoded files are also supported for backwards compatibility. Use --base64 when generating keys if you prefer the base64url format.
When a client connects with a JWT, the relay:
- Decodes the JWT header to extract the
kid(key ID) - Looks up the key from the configured source:
{dir}/{kid}.jwkor{url}/{kid}.jwk - Verifies the JWT signature with the resolved key
- Checks the token's permissions cover the connection path
Key IDs must contain only alphanumeric characters, hyphens, and underscores.
Token Claims
The JWT payload contains these claims:
| Claim | Description |
|---|---|
root | Base path for publish/subscribe permissions |
pub | Suffix appended to root for publish permission |
sub | Suffix appended to root for subscribe permission |
exp | Expiration time (Unix timestamp) |
iat | Issued-at time (Unix timestamp) |
Path Matching
The root claim sets a base path. The pub and sub claims are suffixes:
Full publish path = root + "/" + pub
Full subscribe path = root + "/" + subAn empty suffix ("") allows access to anything under the root.
Examples:
| root | pub | sub | Can publish | Can subscribe |
|---|---|---|---|---|
demo | my-stream | "" | demo/my-stream | demo/* |
rooms/123 | alice | "" | rooms/123/alice | rooms/123/* |
"" | "" | "" | Everything | Everything |
Connection Path
The client's connection URL path does not need to match the token's root exactly. The connection path determines the scope of the session — all publish/subscribe operations are relative to it.
- If the connection path extends the root (e.g., token root=
demo, connect to/demo/room), permissions are narrowed to only paths under/demo/room. - If the connection path is a parent of the root (e.g., token root=
demo, connect to/), permissions still apply but are scoped to the token's root. You can only access paths underdemo/. - If the connection path is unrelated to the root (e.g., token root=
demo, connect to/other), the connection is rejected.
The connection is also rejected if the resulting permissions are empty (no publish or subscribe paths remain after scoping).
Supported Algorithms
Symmetric (HMAC)
The same key signs and verifies. Simpler setup, but the key must be kept secret everywhere it's used.
HS256- HMAC with SHA-256 (default)HS384- HMAC with SHA-384HS512- HMAC with SHA-512
Asymmetric (RSA/ECDSA)
Private key signs, public key verifies. The relay only needs the public key, so compromise of the relay doesn't leak signing capability.
RS256,RS384,RS512- RSA PKCS#1 v1.5PS256,PS384,PS512- RSA PSSES256,ES384- ECDSAEdDSA- Edwards-curve DSA
Anonymous Access
The public setting allows unauthenticated access to a path prefix:
[auth]
key = "my-key.jwk"
public = "anon" # Anyone can publish/subscribe to anon/*Set public = "" to make everything public (development only).
mTLS Peer Authentication
In addition to JWT auth, the relay can authenticate peers via mutual TLS. When the server is configured with a trusted root CA, any client that presents a certificate chaining to that CA is granted full access: root-scoped publish and subscribe permissions plus cluster privileges — equivalent to a JWT with publish: "", subscribe: "", and cluster: true.
This is primarily intended for relay-to-relay (clustering) authentication, as a simpler alternative to distributing long-lived JWTs.
Client certificate presentation is optional: connections without a certificate fall through to the normal JWT path unchanged.
[tls]
cert = ["/etc/moq/server.pem"]
key = ["/etc/moq/server.key"]
# One or more PEM files containing the CAs trusted to sign peer certificates.
root = ["/etc/moq/peer-ca.pem"]The peer's cluster node name is taken from the first DNS SAN on its leaf certificate, so node identity is cryptographically bound to the cert. Certificates without a DNS SAN are still accepted but will not register as cluster nodes.
Only the quinn QUIC backend supports mTLS; configuring tls.root with any other backend is a startup error.
Example Configurations
See the demo/relay/ directory for complete working configuration files, including authentication setup:
- Development -
demo/relay/root.toml(single key with anonymous access) - Production -
demo/relay/prod.toml(key and key directory options)
Library Usage
Rust
rs/moq-token/examples/basic.rs- Symmetric key generation, signing, and verificationrs/moq-token/examples/asymmetric.rs- Asymmetric key pair with public key extraction
TypeScript
See js/token/examples/sign-and-verify.ts for a complete working example of signing and verifying tokens.
See Also
- moq-token (Rust) - Rust library and CLI
- @moq/token - TypeScript library and CLI
- Relay Configuration - Full config reference