Skip to content

v0.5.0 — scope correctness + review sweep

Latest

Choose a tag to compare

@ricbwood ricbwood released this 02 Sep 11:43
94b8660

Fixes the two reported bugs and the review sweep around them. Full detail in #76.

Upgrading — three behaviour changes

1. Grants are resolved against your live config on every request. A scoped token names a fixed set and can now only shrink: one issued while posts:delete was enabled loses it as soon as you disable it, instead of keeping it until expiry. A no-scope token means "everything enabled now" and tracks your config in both directions, as before.

2. A copied mcpPluginOptions now throws at boot (MCP_OPTIONS_NOT_SHARED). Passing a spread or a fresh literal to one plugin and the original to the other was always broken — OAuth tokens 401 while API keys keep working — but it booted. It now fails loudly, naming the mistake. If you hit this on upgrade, assign the options to a const and pass that same const to both mcpPlugin() and payloadMcpOAuth().

3. The login redirect honours routes.admin and admin.routes.login. If you customise either, unauthenticated users mid-flow now reach your real login page rather than a hardcoded /admin/login. A new loginPath option overrides it for sign-in pages outside the admin panel.

Fixes

  • #75 — tokens issued without a scope recorded capabilities: {}, which read as "no permissions" to anything inspecting the row while requests ran with full access via a fallback. Claude.ai's Custom Connector completes the flow without a scope, so this affected every such token. The row now records the real grant; scopeToCapabilities returns a discriminated result so "no scope requested" and "invalid scope" can no longer be confused. Existing tokens keep working — no migration, no backfill.
  • #71 — a http://localhost issuer no longer blocks a local next build. next build/next start set NODE_ENV=production whether or not you are deploying, and a loopback URL is unreachable off the machine. Non-loopback http issuers are still refused; a loopback one in production now warns, in case it is an unset SERVER_URL on a real deployment.
  • Scoped tokens previously used their stored capabilities verbatim, so a grant could outlive the permission that justified it.
  • iss is now set on error and deny authorization responses, not just success (RFC 9207 §2).
  • A malformed oauth-clients row returned a 500 instead of invalid_redirect_uri.

Additions

  • scopes_supported in both discovery documents, derived from your mcpPluginOptions — clients can now discover what to request. Also advertises authorization_response_iss_parameter_supported.
  • loginPath option.
  • The plugin registers slug plugin-mcp-oauth with options and a RegisteredPlugins augmentation, for cross-plugin discovery.

Docs

Scopes are documented for the first time. The adminAccess default was described as "any authenticated user in your userCollection", omitting the role check — if your operators carry a role other than admin, they are locked out of the OAuth screens and now the README says so. threat-model.md T2/E1 described the pre-narrowing design and are rewritten.

Also

#50 endpoints are derived rather than mutated in place. #51 the copied-options detection above, plus a record of the two alternatives that are structurally impossible. #52 plugin slug/order/options set directly — deliberately not via definePlugin, which only exists from payload 3.83.0 while the peer range is ^3.0.0. #53 closed: Payload's KV store has no compare-and-set, so moving auth codes to it would make single-use redemption a race.

Verified with 434 unit tests and the from-scratch packaged install test (31/31), which packs the tarball into a fresh Payload app and runs the full OAuth handshake.