v0.2.0 — Scope narrowing, single-use CSRF nonces + security hardening
A substantial security release bundling everything since 0.1.7 (#38–#41). Upgrade recommended.
✨ Features
- Per-scope capability narrowing (#35). Access tokens are now granted only the MCP capabilities their OAuth
scopemaps to (<slug>:read|write|delete), instead of always the full operator grant. The operator-configured set remains the hard ceiling; an empty scope still grants the full set. An invalid/no-longer-grantable scope is rejected withinvalid_scope(no silent escalation). - Single-use, time-bound CSRF nonces (#27). Consent CSRF tokens are session-bound, expiring, and now single-use, backed by the new
oauth-csrf-noncescollection (with an efficient bulk sweep).
🔒 Security hardening
- Consent endpoint is session-bound — identity is derived from
req.user, never the request body; a mismatching bodyuser_idis rejected (#38). - CSRF/consent input hardened — non-string token guard, numeric
user_idcoercion (#39). - IP-only rate-limit key — rotating
client_id/client_namecan no longer mint fresh buckets and bypass the per-IP limit; empty/whitespace IPs coalesced (#40, #41). /register: reject redirect URIs with a fragment (RFC 6749 §3.1.2), allow IPv6 loopback[::1], and capsoftware_id/software_version(#40, #41).- Closed backlog issues #26 #28 #29 #30 #31 and reconciled the threat model (#40).
Upgrade
pnpm add @brainwebuk/payload-plugin-mcp-oauth@0.2.0No config changes required. If you adopt scoped tokens, requested scopes use <collectionOrGlobalSlug>:read|write|delete.