Authentication
The server authorizes a request by a credential header or by a signed URL.
| Method | Flag | Client sends |
|---|---|---|
| Basic | --server-basic-auth | Authorization: Basic <base64 of user:password> |
| Token | --server-token-auth | Authorization: Bearer <token> |
| Delegated | --server-delegated-auth | A Bearer or Basic header the configured service or program accepts |
| Signed URL | --server-signed-url | expires and signature query parameters |
A missing or wrong credential answers 401. With both Basic and token set, either authorizes. A delegate sees only the credentials neither accepts.
Precedence
A valid credential header authorizes outright, and a wrong one is rejected without checking a signature. A request with no credential header is checked for a signature when signed URLs are on.
Signatures apply to GET /, DELETE / and GET /assets. POST / and /mcp authorize by credential header only, so a server with signed URLs and no Basic or token auth accepts any POST and any MCP request.
Delegated Authentication
--server-delegated-auth names an https:// URL, an http:// URL on a loopback host, or a command. The server sends it each credential the static flags do not accept, as JSON with a fresh nonce:
{"type": "token", "token": "…", "nonce": "9f2c…"}
{"type": "basic", "username": "…", "password": "…", "nonce": "9f2c…"}A URL receives it as a POST; a command receives it on stdin and nothing in its arguments or environment. The command is pinned at startup: the server refuses to run it once the file changes.
| Delegate answers | Server answers |
|---|---|
{"authenticated": true, "nonce": <the same>}, with 200 or exit 0 | The request proceeds |
{"authenticated": false}, 401, 403 or a non-zero exit | 401 |
Anything else, or nothing within --server-delegated-auth-timeout | 503 |
An approval without the request’s nonce is not one, so no generic page, cached reply or fixed file can approve a request. The server asks for every request; it remembers no answer.
A command that approves one token:
#!/bin/sh
payload=$(cat)
nonce=$(printf '%s' "$payload" | sed -n 's/.*"nonce":"\([0-9a-f]*\)".*/\1/p')
case "$payload" in
*'"token":"s3cr3t"'*) printf '{"authenticated":true,"nonce":"%s"}' "$nonce" ;;
*) exit 1 ;;
esacSigned URLs
Sign the request’s query parameters, expires included:
- Set
expiresto a Unix time in seconds. - Sort the parameters by key, then by value, and form-encode them.
- Compute HMAC-SHA256 of that string with the secret, as lowercase hex.
- Append it as
signature.
import hashlib, hmac, time, urllib.parse
params = {
"url": "https://zshot-cli.com/example_assets/zrm/",
"output_type": "png",
"expires": str(int(time.time()) + 300),
}
query = urllib.parse.urlencode(sorted(params.items()))
signature = hmac.new(b"SECRET", query.encode(), hashlib.sha256).hexdigest()
print("http://127.0.0.1:3000/?" + query + "&signature=" + signature)| Response | Reason |
|---|---|
401 | no signature |
403 | the signature does not match, expires is missing or past, or expires is beyond --server-signed-url-max-age |
The server signs the asset links in a render response with the same secret; see Additional Assets.
Metrics Endpoint
--server-prometheus-auth sets a Basic credential that is the only one /metrics accepts. Without it, /metrics takes the main server’s credentials, and is open when the server has none.