BrenzuriStart free
Authentication
Reference

Authentication

Two credential kinds, one header. A credential is bound to a prefix, a path and usually one site.

On this page

Three credentials, one header#

CredentialPrefixWorks onMade byLifetimeBound to
API keybz_live_/api/v1/…A person, in Developer › API keysUntil revokedOne site, or the whole workspace if an Owner made it
Agent tokenbz_mcp_live_/mcp/v1A person, in Developer › MCP serverUntil revokedOne site
OAuth access tokenbz_mcp_live_/mcp/v1Consent page, through an MCP client1 hour; its refresh token lasts 30 daysOne site

A token is its prefix and 43 URL-safe characters. Brenzuri keeps only a SHA-256 hash, so a token is shown once, when it is made, and cannot be shown again.

curl "$BRENZURI_URL/api/v1/sites" \
  -H "Authorization: Bearer $BRENZURI_KEY"
Every request carries the token as a bearer credential.

The prefix is tied to the path. An agent token on a REST route, or a key on /mcp, is refused as if it were unknown. The header name Bearer is matched case-insensitively. A bearer token wins over a session cookie when both are sent.

What a bad credential gets#

  • An unknown, revoked or wrongly prefixed token gets 401 invalid_key with WWW-Authenticate: Bearer error="invalid_token". On /mcp the challenge also carries a resource_metadata address, which is how an OAuth client finds the sign-in.
  • More than 20 refused tokens a minute from one client address are answered 429 rate_limited, whatever the token.
  • A credential whose creator has left the workspace, or whose site was deleted, is refused as unknown.
  • A deleted workspace answers 410 workspace_deleted, with deletedAt and restoreUntil.
  • A route not on the allowlist answers 403 key_not_allowed; a missing scope answers 403 scope_missing. See Roles and scopes.

Creating and managing keys#

Keys are made by a signed-in person, never by another key. These routes need a session and are not reachable with a bearer token.

RouteWhat it does
GET /api/v1/developer/keysLists keys. An Owner sees all of them; anyone else sees their own.
POST /api/v1/developer/keysMakes a key from { name, siteId?, scopes[], dailyCap } and returns { key, token }. The token appears only here.
PATCH /api/v1/developer/keys/{keyID}Changes name or dailyCap. Nothing else.
DELETE /api/v1/developer/keys/{keyID}Revokes the key. 204.
  • name is 1 to 80 characters. scopes has at least one of brief, generate, read, edit, export. dailyCap is 0 to 1000 credits.
  • Leaving out siteId makes a whole-workspace key, which only an Owner can do. Anyone else gets 403 whole_workspace_owner_only.
  • The scopes and the site cannot be changed after creation. To change them make a new key.
  • A key has no expiry and there is no rotation route. To rotate, make a new key, switch your integration to it, then revoke the old one.
  • When a person leaves the workspace their keys are revoked.

Agent tokens#

POST /api/v1/developer/mcp/agents makes an agent token from { name, siteId, scopes[], dailyCap }. An agent is always bound to one site. The workspace plan limits how many agents are connected; a start over the limit is 409 plan_limit. Agents made by OAuth appear in the same list.

OAuth 2.1 for MCP#

A client that speaks OAuth gets an agent without anyone pasting a token. The server is a public-client, authorization-code-with-PKCE server and nothing else.

All on the Brenzuri host. Both metadata documents are served from the same host.
Document or routeAddress
Protected resource metadata/.well-known/oauth-protected-resource and /.well-known/oauth-protected-resource/mcp/v1
Authorization server metadata/.well-known/oauth-authorization-server
Authorization page (a browser, sign-in required)/oauth/authorize
Client registrationPOST /api/v1/oauth/register
TokenPOST /api/v1/oauth/token
RevocationPOST /api/v1/oauth/revoke
JSON
{
  "issuer": "https://{your Brenzuri host}",
  "authorization_endpoint": "https://{your Brenzuri host}/oauth/authorize",
  "token_endpoint": "https://{your Brenzuri host}/api/v1/oauth/token",
  "registration_endpoint": "https://{your Brenzuri host}/api/v1/oauth/register",
  "revocation_endpoint": "https://{your Brenzuri host}/api/v1/oauth/revoke",
  "response_types_supported": [
    "code"
  ],
  "grant_types_supported": [
    "authorization_code",
    "refresh_token"
  ],
  "code_challenge_methods_supported": [
    "S256"
  ],
  "token_endpoint_auth_methods_supported": [
    "none"
  ],
  "scopes_supported": [
    "brief",
    "generate",
    "read",
    "edit",
    "export"
  ],
  "client_id_metadata_document_supported": true,
  "authorization_response_iss_parameter_supported": true
}
The authorization server metadata, with the issuer filled in. Fields the server also sends, such as the revocation auth methods, are left out.

Identify the client#

Either register one, or use a client id that is an https address.

  • Dynamic registration. POST /api/v1/oauth/register with { client_name?, redirect_uris[] }. One to five redirect URIs, each https or an http loopback address (localhost, 127.0.0.1, [::1]), at most 512 characters, no fragment. The answer is 201 with client_id. A loopback redirect may use any port.
  • Client ID metadata document. A client_id that is an https URL with a path. Brenzuri fetches it (16 KB at most, 5 seconds, no redirects), and it must repeat the client_id and list one to ten valid redirect_uris. It is cached for an hour.
Shell
curl -X POST "$BRENZURI_URL/api/v1/oauth/register" \
  -H "Content-Type: application/json" \
  -d '{ "client_name": "My agent", "redirect_uris": ["http://127.0.0.1:8765/callback"] }'

Build an authorization address with a PKCE S256 challenge and open it in a browser. The challenge is the base64url of the SHA-256 of a random verifier of 43 to 128 characters.

VERIFIER=$(openssl rand -base64 48 | tr -d '=+/' | cut -c1-64)
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
echo "$CHALLENGE"
Query parameterValue
response_typecode
client_idThe registered id, or the https metadata address.
redirect_uriOne of the registered addresses.
code_challenge, code_challenge_methodThe 43-character challenge and S256. No other method is accepted.
stateOptional. Returned unchanged.
resourceOptional. If sent it must be the MCP server address, https://{your Brenzuri host}/mcp/v1.
scopeOptional, space-separated. Without it, or with unknown scopes, all five are offered.

The person signs in if they are not, and the page asks them for four things: one site, the scopes they allow (never more than were asked for), a daily credit cap (5 if left alone, 0 to 1000), and a name. They need a role that can edit. Pressing Allow redirects to your redirect_uri with code=bz_code_…, state and iss; pressing Deny redirects with error=access_denied. A code lasts 10 minutes and works once.

Exchange the code#

Shell
curl -X POST "$BRENZURI_URL/api/v1/oauth/token" \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri="$REDIRECT_URI" \
  -d client_id="$CLIENT_ID" \
  -d code_verifier="$VERIFIER"
JSON
{
  "access_token": "bz_mcp_live_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "bz_mcp_refresh_…",
  "scope": "brief generate read edit export"
}
  • The access token is an agent token and lasts one hour. Send it to /mcp/v1 as a bearer token.
  • To renew, POST /api/v1/oauth/token with grant_type=refresh_token, refresh_token and client_id. Refresh tokens rotate: each use returns a new one, and presenting an old one again revokes the agent. A refresh checks that the person may still hold that site.
  • POST /api/v1/oauth/revoke with token (and optionally client_id) always answers 200.
  • Token and revocation bodies are form-encoded or JSON. Errors have the OAuth shape { "error", "error_description" }: invalid_request, invalid_client (401), invalid_grant, unsupported_grant_type, invalid_target, invalid_redirect_uri.