Files
worldmonitor/api/_crypto.js
Elie Habib 51f7b7cf6d feat(mcp): full OAuth 2.1 compliance — Authorization Code + PKCE + DCR (#2432)
* feat(mcp): full OAuth 2.1 compliance — Authorization Code + PKCE + DCR

Adds the complete OAuth 2.1 flow required by claude.ai (and any MCP
2025-03-26 client) on top of the existing client_credentials path.

New endpoints:
- POST /oauth/register — Dynamic Client Registration (RFC 7591)
  Strict allowlist: claude.ai/claude.com callbacks + localhost only.
  90-day sliding TTL on client records (no unbounded Redis growth).

- GET/POST /oauth/authorize — Consent page + authorization code issuance
  CSRF nonce binding, X-Frame-Options: DENY, HTML-escapes all metadata,
  shows exact redirect hostname, Origin validation (must be our domain).
  Stores full SHA-256 of API key (not fingerprint) in auth code.

- authorization_code + refresh_token grants in /oauth/token
  Both use GETDEL for atomic single-use consumption (no race condition).
  Refresh tokens carry family_id for future family invalidation.
  Returns 401 invalid_client when DCR client is expired (triggers re-reg).

Security improvements:
- verifyPkceS256() validates code_verifier format before any SHA-256 work;
  returns null=invalid_request vs false=invalid_grant.
- Full SHA-256 (64 hex) stored for new OAuth tokens; legacy
  client_credentials keeps 16-char fingerprint (backward compat).
- Discovery doc: only authorization_code + refresh_token advertised.
- Protected resource metadata: /.well-known/oauth-protected-resource
- WWW-Authenticate headers include resource_metadata param.
- HEAD /mcp returns 200 (Anthropic probe compatibility).
- Origin validation on POST /mcp: claude.ai/claude.com + absent allowed.
- ping method + tool annotations (readOnlyHint, openWorldHint).
- api/oauth/ subdir added to edge-function module isolation scan.

* fix(oauth): distinguish Redis unavailable from key-miss; fix retry nonce; extend client TTL on token use

P1: redisGetDel and redisGet in token.js and authorize.js now throw on
transport/HTTP errors instead of swallowing them as null. Callers catch
and return 503 so clients know to retry, not discard valid codes/tokens.
Key-miss (result=null from Redis) still returns null as before.

P2a: Invalid API key retry path now generates and stores a fresh nonce
before re-rendering the consent form. Previously the new nonce was never
persisted, causing the next submit to fail with "Session Expired" and
forcing the user to restart the entire OAuth flow on a single typo.

P2b: token.js now extends the client TTL (EXPIRE, fire-and-forget) after
a successful client lookup in both authorization_code and refresh_token
paths. CLIENT_TTL_SECONDS was defined but unused — clients that only
refresh tokens would expire after 90 days despite continuous use.

* fix(oauth): atomic nonce consumption via GETDEL; fail closed on nonce storage failure

P2a: Nonce storage result is now checked before rendering the consent page
(both initial GET and invalid-key retry path). If redisSet returns false
(storage unavailable), we return a 503-style error page instead of
rendering a form the user cannot submit successfully.

P2b: CSRF nonce is now consumed atomically via GETDEL instead of a
read-then-fire-and-forget-delete. Two concurrent POST submits can no
longer both pass validation before the delete lands, and the delete is
no longer vulnerable to edge runtime isolate teardown.
2026-03-28 18:40:53 +04:00

2.0 KiB