Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ listeners:
# max_data: 33554432 # Override connection flow control window for this listener

service_defaults:
# Default cache settings inherited by all services.
# Per-service cache merges field-by-field: set only the fields you want to override.
cache:
enabled: true # Enable relay cache
Expand All @@ -39,6 +38,10 @@ service_defaults:
# default_max_cache_duration_s: 300 # Default TTL (seconds) for tracks without a publisher-set duration.
# Absent: use max_cache_duration_s.
# 0: opt-in-only (don't cache tracks unless publisher sets a duration).
# Required (directly or via this block) whenever a service sets auth.enabled: true.
auth:
max_tokens_per_message: 4 # Caps AUTHORIZATION_TOKEN params of the service's token_type per
# message; a message over the cap is rejected outright, not truncated.

services:
live-streaming:
Expand Down Expand Up @@ -70,8 +73,12 @@ services:
# - id: "cat-dev" # Key ID carried in the token envelope; selects which secret verifies it
# secret: "replace-with-long-random-secret" # Shared secret used by moqx-issuer
# require_setup_token: true # Require a client_setup token during CLIENT_SETUP
# allow_request_token_override: true # Allow a per-request token to override the session's setup grants
# allow_request_token_override: true # Let a per-request token count as one more candidate grant, alongside setup-token grants and anonymous_claim
# strict_claims: false # Keep false for current CAT4MOQ interop unless all issuers send only supported claims
# max_tokens_per_message: 4 # Required directly or via service_defaults.auth; caps AUTHORIZATION_TOKEN params verified per message
# anonymous_claim: # Statically-granted scopes applied to every request, with or without a token
# - actions: [subscribe, fetch] # Anyone can subscribe/fetch under "live/..." without a token
# namespace_match: {prefix: ["live"]}

testing:
match:
Expand Down
61 changes: 57 additions & 4 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ listener_defaults: # optional; QUIC defaults inherited by all listeners
listeners: # required; at least one
- { ... }

service_defaults: # optional; cache defaults inherited by all services
service_defaults: # optional; cache and auth defaults inherited by all services
cache: { ... }
auth: { max_tokens_per_message: 4 }

services: # required; at least one
my-service:
Expand Down Expand Up @@ -165,20 +166,71 @@ services:
require_setup_token: true
allow_request_token_override: true
strict_claims: false
max_tokens_per_message: 4
anonymous_claim:
- actions: [subscribe, fetch]
namespace_match: {prefix: ["live"]}
```

| Field | Default | Notes |
|---|---|---|
| `enabled` | `false` | Enables CAT-style authorization for this service. |
| `enabled` | `false` | Enables CAT-style authorization for this service. When `false`, nothing is enforced and the other `auth` fields have no effect. |
| `token_type` | `0` | MOQT `AUTHORIZATION_TOKEN` type to accept. Use `16` with CAT4MOQ tokens produced for moqxr's CAT wrapper. Type `0` is valid for private or out-of-band deployments. The value must fit in a QUIC variable integer. |
| `hmac_keys` | empty | Required when `enabled: true`. Each key needs a non-empty `id` and `secret`; duplicate key IDs are rejected. The token issuer must use the same key ID and secret. |
| `require_setup_token` | `true` | Requires a valid setup token authorizing `client_setup` during CLIENT_SETUP. If `false`, clients can connect without setup grants, but publish/subscribe requests still need an authorized setup or request token. |
| `allow_request_token_override` | `true` | Allows a token on a request to replace the session setup grants for that request. If `false`, request tokens are ignored and authorization uses only the setup token grants. |
| `require_setup_token` | `true` | Requires a valid setup token authorizing `client_setup` during session setup. If `false`, clients can connect without setup grants; per-request actions still need an authorized token or a matching `anonymous_claim` entry. |
| `allow_request_token_override` | `true` | Lets a token on a request count as one more candidate grant for that request, alongside the session's setup-token grants and `anonymous_claim`. If `false`, request tokens are ignored and authorization uses only the setup token grants and `anonymous_claim`. |
| `strict_claims` | `false` | Rejects unsupported claims when `true`. Keep this `false` for current CAT4MOQ interop unless every issuer is known to send only supported claims. |
| `max_tokens_per_message` | none | Required when `enabled: true`, directly or via `service_defaults.auth.max_tokens_per_message`. Caps how many `AUTHORIZATION_TOKEN` params of the configured `token_type` a single SETUP or request message may carry; a message over the cap is rejected outright, not truncated. Params of other token types are not counted. Bounds per-message verification cost against a client attaching many tokens to one message. Must be >= 1. |
| `anonymous_claim` | empty | Statically-granted scopes applied to every request on this service, regardless of what token (if any) authenticated it. See [Anonymous Claim](#anonymous-claim) below. |

The relay only verifies tokens; it does not call an external grant handler.
Grant decisions are encoded by the token issuer as CAT4MOQ actions and scopes.

A message (SETUP or any per-request message) may carry more than one
`AUTHORIZATION_TOKEN` of the configured `token_type`, up to
`max_tokens_per_message`; a message carrying more than that is rejected
outright. Within the cap, a request is authorized if
any one of the tokens present verifies and covers the action; a token that
fails to verify is simply not counted as a candidate rather than failing the
request outright, as long as some other token, the session's setup grants, or
`anonymous_claim` covers it.

### Anonymous Claim

`anonymous_claim` grants a floor of access to every request on the service,
whether or not the request carries a token at all, whether or not a setup
token was presented, and regardless of what any presented token grants. It
never authorizes `client_setup`/`server_setup` — connecting still needs a
valid setup token whenever `require_setup_token: true`; the claim only ever
widens what an already-connected session (or an anonymous one, when
`require_setup_token: false`) can do per request.

```yaml
auth:
enabled: true
hmac_keys: [{id: "cat-dev", secret: "replace-with-long-random-secret"}]
require_setup_token: true
max_tokens_per_message: 4
anonymous_claim:
- actions: [subscribe, fetch]
namespace_match: {prefix: ["live"]}
track_match: {exact: "video"}
```

Each entry:
- `actions` — one or more action names from the table below (aliases and
numeric IDs accepted, same as `moqx-issuer`'s `--auth-actions`).
`client_setup`/`server_setup` are rejected.
- `namespace_match` — optional; a list of namespace segments matched by
`exact`, `prefix`, `suffix`, or `contains`. Omit to match any namespace.
- `track_match` — optional; a single track name matched by `exact`, `prefix`,
`suffix`, or `contains`. Omit to match any track.

The four match modes are the same `BinaryMatchType` (exact/prefix/suffix/contains)
CAT4MOQ tokens use for their own namespace/track claims, so an `anonymous_claim`
scope matches namespaces and tracks with identical semantics to a token-granted
scope.

### Issuing Tokens

Use the standalone `moqx-issuer` binary as the deployment/operator tool. For
Expand Down Expand Up @@ -284,6 +336,7 @@ key ID, waiting for old tokens to expire, then removing the old key.
namespace, or wrong track is rejected.
- Auth is currently service-local. Upstream relay connections still have no
application-level credential exchange beyond TLS.
- `max_tokens_per_message` is inheritable from `service_defaults.auth`

---

Expand Down
1 change: 1 addition & 0 deletions src/MoqxRelayContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ folly::Expected<folly::Unit, SessionCloseErrorCode> MoqxRelayContext::validateAu
case auth::AuthError::Forbidden:
case auth::AuthError::Missing:
case auth::AuthError::WrongTokenType:
case auth::AuthError::TooManyTokens:
return folly::makeUnexpected(SessionCloseErrorCode::UNAUTHORIZED);
}
return folly::makeUnexpected(SessionCloseErrorCode::UNAUTHORIZED);
Expand Down
104 changes: 104 additions & 0 deletions src/auth/Action.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright (c) OpenMOQ contributors.
* This source code is licensed under the Apache 2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#include "auth/ExhaustiveSwitch.h"

#include <folly/String.h>

#include <algorithm>
#include <cctype>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>

// CAT4MOQ actions and their canonical names. Lightweight (moxygen-free) on
// purpose.
namespace openmoq::moqx::auth {

enum class Action : uint64_t {
ClientSetup = 0,
ServerSetup = 1,
PublishNamespace = 2,
SubscribeNamespace = 3,
Subscribe = 4,
RequestUpdate = 5,
Publish = 6,
Fetch = 7,
TrackStatus = 8,
};

enum class MatchRuleType : uint64_t { Exact = 0, Prefix = 1, Suffix = 2, Contains = 3 };

ENFORCE_EXHAUSTIVE_SWITCH_BEGIN
inline std::string_view actionName(Action action) {
switch (action) {
case Action::ClientSetup:
return "client_setup";
case Action::ServerSetup:
return "server_setup";
case Action::PublishNamespace:
return "publish_namespace";
case Action::SubscribeNamespace:
return "subscribe_namespace";
case Action::Subscribe:
return "subscribe";
case Action::RequestUpdate:
return "request_update";
case Action::Publish:
return "publish";
case Action::Fetch:
return "fetch";
case Action::TrackStatus:
return "track_status";
}
return "unknown";
}
ENFORCE_EXHAUSTIVE_SWITCH_END

// Canonicalizes an action name (aliases, numeric IDs, case/dash-insensitive)
// to its Action, or nullopt if unrecognized.
inline std::optional<Action> canonicalAction(std::string_view name) {
std::string normalized =
folly::trimWhitespace(folly::StringPiece(name.data(), name.size())).str();
std::replace(normalized.begin(), normalized.end(), '-', '_');
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});

if (normalized == "client_setup" || normalized == "setup" || normalized == "0") {
return Action::ClientSetup;
}
if (normalized == "server_setup" || normalized == "1") {
return Action::ServerSetup;
}
if (normalized == "publish_namespace" || normalized == "announce" || normalized == "2") {
return Action::PublishNamespace;
}
if (normalized == "subscribe_namespace" || normalized == "3") {
return Action::SubscribeNamespace;
}
if (normalized == "subscribe" || normalized == "4") {
return Action::Subscribe;
}
if (normalized == "request_update" || normalized == "subscribe_update" || normalized == "5") {
return Action::RequestUpdate;
}
if (normalized == "publish" || normalized == "6") {
return Action::Publish;
}
if (normalized == "fetch" || normalized == "7") {
return Action::Fetch;
}
if (normalized == "track_status" || normalized == "8") {
return Action::TrackStatus;
}
return std::nullopt;
}

} // namespace openmoq::moqx::auth
Loading
Loading