fix(security): require auth on every MCP request; stop stale refreshes banning clients

- Remove the authless "sole connected phone" fallback and SB_REQUIRE_MCP_AUTH:
  an unauthenticated request no longer reaches whichever phone is online alone.
- Mcp-Session-Id is no longer a credential; the Bearer token is checked on
  every request (MCP auth spec).
- Refresh tokens live 90 days (was 30, equal to the access token, so they
  were always dead when first needed).
- A rejected refresh no longer counts toward the IP ban: a client with an
  expired token was retrying into a self-renewing ban on its own IP.
- 401s carry the RFC 9728 WWW-Authenticate discovery header.
This commit is contained in:
AletheiaVox
2026-09-27 12:55:16 +02:00
parent 605f71b742
commit 7bb9d8473b
9 changed files with 124 additions and 53 deletions

View File

@@ -2,8 +2,32 @@
## Unreleased
### Security
- **Removed the authless "sole connected phone" fallback.** With
`SB_REQUIRE_MCP_AUTH=false` (the old default), any unauthenticated MCP
request was routed to whichever phone was connected, as long as it was the
only one. On a shared server that means: a stranger connects while you're
the only one online and gets your hardware. Every MCP request now needs a
valid Bearer token (OAuth or login JWT). `SB_REQUIRE_MCP_AUTH` is gone; an
old `.env` that still sets it is harmless.
- **`Mcp-Session-Id` is no longer a credential.** A session ID used to stand
in for the token on follow-up requests, so it kept working after the
token expired or was revoked. The Bearer token is now checked on every
request, as the MCP auth spec requires.
### Fixed
- **Clients no longer ban their own IP when their token expires.** Refresh
tokens lasted 30 days, the same as a 720-hour access token, and clients
only refresh once the access token is dead, so the refresh token was
always dead too. Every rejected refresh counted as a failed login, and a
client retrying on a timer reached `SB_BAN_THRESHOLD` in about half an
hour, then got banned again every time the ban lapsed. Refresh tokens now
last 90 days, and rotation issues a new one on every refresh. A rejected
refresh (the client has already proven its `client_secret`) no longer
counts toward the ban. Wrong passwords and wrong client secrets still do.
- **Disabling the governor now disables the governor.** `enabled` gated only
`Governor.check()` — the enforcement call. The heat model kept integrating
on every heartbeat, the state kept riding along on heartbeat pings, and