feat: authenticate as an OAuth resource server against mcp-auth #3

Merged
airaneel merged 1 commit from oauth-resource-server into main 2026-08-08 02:49:00 +03:00
Owner

babash on a public port is an unauthenticated root shell — anyone who reaches
it can call babash_initialize and get one. chat_id isolates conversations
from each other, but it is a value the model carries, not a secret, so it keeps
no stranger out.

This makes babash a resource server in the fleet's existing OAuth: it
validates tokens and serves tools, mcp-auth mints them. No /authorize,
/token or client registration lives here — passing the SDK only a
token_verifier mounts the resource-server half and leaves the rest unmounted.
That absence is load-bearing: advertising a registration endpoint we cannot
serve would send compliant clients into it, and they abandon the flow rather
than fall back.

Two gates, for two different jobs

  • Scope — checked by the transport. This is what lets one issuer safely
    serve a whole fleet: a token minted for websearch carries websearch and
    will not open a shell here.
  • Licence — checked in verify_token. A scope cannot say who you are,
    since anyone who can log in may ask for one. The per-service licence is the
    record that names a person and can be withdrawn from one without disturbing
    anyone else.

A caller holding no babash licence is refused at verification, before there
is a tool to call. Paid siblings do the opposite — admit the caller, refuse the
tool with a note about buying — which is right for something a stranger is
meant to buy into, and wrong for a root shell, where nothing is for sale.

Metadata at the bare well-known path too

Not only at the RFC 9728 §3.1 path the 401 points at. Some clients probe the
bare one first and read a 404 as "no OAuth here" — and more concretely, the
nginx in front of this proxies discovery to the upstream's bare path:

/.well-known/oauth-protected-resource/mcp-servers/babash/… → upstream /.well-known/oauth-protected-resource

so without it discovery 404s in the deployment this is for. It is built by the
same SDK call from the same three values, so there is one document, not a copy
free to drift.

Off by default

Auth is on exactly when MCP_AUTH_URL is set, so stdio and local dev are
unchanged. Set without PUBLIC_URL, the server refuses to start rather than
serve on a resource URL it invented — one clients would pin against and then be
unable to reach.

Verification

  • 112 passed (96 before, no regressions) — 16 new tests covering the
    200 + null trap, the licence gate's fail-closed directions, an unreachable
    issuer, the 401 challenge, metadata at both paths, and that no AS endpoint is
    advertised.
  • mypy --strict src/babash clean, ruff check clean.

⚠️ Deployment ordering — issue the licence first

The licence gate means enabling auth before issuing yourself a babash licence
locks you out of babash-remote. Correct order, in andyjazzy.cloud:

  1. mcp_auth: add babash to mcp_auth_extra_scopes + prefix babash: BB, redeploy
  2. issue yourself a babash licence and bind it on the mcp-auth login form
  3. babash stack env: MCP_AUTH_URL, PUBLIC_URL=https://raidmen.ru/mcp-servers/babash, REQUIRED_SCOPE=babash
  4. nginx.yml: babash needs_oauth: falsetrue
  5. build/push image, docker pull on raidmen, redeploy

🤖 Generated with Claude Code

babash on a public port is an unauthenticated root shell — anyone who reaches it can call `babash_initialize` and get one. `chat_id` isolates conversations from each other, but it is a value the model carries, not a secret, so it keeps no stranger out. This makes babash a **resource server** in the fleet's existing OAuth: it validates tokens and serves tools, `mcp-auth` mints them. No `/authorize`, `/token` or client registration lives here — passing the SDK only a `token_verifier` mounts the resource-server half and leaves the rest unmounted. That absence is load-bearing: advertising a registration endpoint we cannot serve would send compliant clients into it, and they abandon the flow rather than fall back. ## Two gates, for two different jobs - **Scope** — checked by the transport. This is what lets one issuer safely serve a whole fleet: a token minted for websearch carries `websearch` and will not open a shell here. - **Licence** — checked in `verify_token`. A scope cannot say *who you are*, since anyone who can log in may ask for one. The per-service licence is the record that names a person and can be withdrawn from one without disturbing anyone else. A caller holding no `babash` licence is refused at verification, before there is a tool to call. Paid siblings do the opposite — admit the caller, refuse the tool with a note about buying — which is right for something a stranger is meant to buy into, and wrong for a root shell, where nothing is for sale. ## Metadata at the bare well-known path too Not only at the RFC 9728 §3.1 path the 401 points at. Some clients probe the bare one first and read a 404 as "no OAuth here" — and more concretely, the nginx in front of this proxies discovery to the upstream's *bare* path: ``` /.well-known/oauth-protected-resource/mcp-servers/babash/… → upstream /.well-known/oauth-protected-resource ``` so without it discovery 404s in the deployment this is for. It is built by the same SDK call from the same three values, so there is one document, not a copy free to drift. ## Off by default Auth is on exactly when `MCP_AUTH_URL` is set, so stdio and local dev are unchanged. Set without `PUBLIC_URL`, the server refuses to start rather than serve on a resource URL it invented — one clients would pin against and then be unable to reach. ## Verification - `112 passed` (96 before, no regressions) — 16 new tests covering the `200 + null` trap, the licence gate's fail-closed directions, an unreachable issuer, the 401 challenge, metadata at both paths, and that no AS endpoint is advertised. - `mypy --strict src/babash` clean, `ruff check` clean. ## ⚠️ Deployment ordering — issue the licence first The licence gate means enabling auth before issuing yourself a `babash` licence locks you out of babash-remote. Correct order, in `andyjazzy.cloud`: 1. `mcp_auth`: add `babash` to `mcp_auth_extra_scopes` + prefix `babash: BB`, redeploy 2. **issue yourself a `babash` licence and bind it** on the mcp-auth login form 3. babash stack env: `MCP_AUTH_URL`, `PUBLIC_URL=https://raidmen.ru/mcp-servers/babash`, `REQUIRED_SCOPE=babash` 4. `nginx.yml`: babash `needs_oauth: false` → `true` 5. build/push image, `docker pull` on raidmen, redeploy 🤖 Generated with [Claude Code](https://claude.com/claude-code)
babash on a public port is an unauthenticated root shell: anyone who
reaches it can call babash_initialize and get one. chat_id isolates
conversations from each other, but it is a value the model carries, not
a secret, so it keeps no stranger out.

So babash joins the fleet's existing OAuth as a *resource server* and
nothing more. It validates tokens and serves tools; mcp-auth mints them.
No /authorize, no /token, no client registration lives here — passing
the SDK only a token_verifier is what mounts the resource-server half and
leaves the rest unmounted. That absence is load-bearing: advertising a
registration endpoint we cannot serve would send compliant clients into
it, and they abandon the flow rather than fall back.

Two gates, for two different jobs. The scope is checked by the transport
and is what lets one issuer safely serve a fleet — a token minted for
websearch carries `websearch` and will not open a shell here. What a
scope cannot do is say who you are, since anyone who can log in may ask
for one; so the second gate is the per-service licence, the record that
names a person and can be withdrawn from one without disturbing anyone
else. A caller holding no babash licence is refused at verification,
before there is a tool to call. Paid siblings do the opposite — admit the
caller, refuse the tool with a note about buying — which is right for
something a stranger is meant to buy into, and wrong for a root shell,
where nothing is for sale.

The metadata document is also served at the bare well-known path, not
only at the RFC 9728 §3.1 path the 401 points at. Some clients probe the
bare one first and read a 404 as "no OAuth here"; more concretely, the
nginx in front of this proxies discovery to the upstream's bare path, so
without it discovery 404s in the deployment this is for. It is built by
the same SDK call from the same three values, so there is one document,
not a copy free to drift.

Off unless MCP_AUTH_URL is set, which keeps stdio and local dev exactly
as they were. Set without PUBLIC_URL it refuses to start rather than
serve on a resource URL it invented — one clients would pin against and
then be unable to reach.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
airaneel deleted branch oauth-resource-server 2026-08-08 02:49:00 +03:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
airaneel/babash!3
No description provided.