Skip to content

Resources & Scopes

A resource is the thing AuthPlane issues tokens for. Every MCP server you protect, and every upstream provider you vend tokens for, is one row in authserver’s resources table. The backend_kind column tells AuthPlane what to do when a client requests a token for that resource.

BackendWhat AuthPlane returnsWhen to use
MintAuthPlane-signed JWT (at+jwt)Default for new MCP servers — they trust AuthPlane’s JWKS and verify offline.
BrokerThe user’s upstream-provider access token (e.g. a GitHub gho_…)You want the MCP server to call GitHub/Slack/Google as the user, using the user’s consent.
FrontedAuthPlane hands off to a downstream resource server that mints its own tokenYou have an existing resource server with its own authorization model and can’t re-architect it.

These aren’t mutually exclusive within a deployment — an MCP server might be a Mint resource for its own tools and also call out to a Broker resource (GitHub) on behalf of the user. Mint and Broker are covered in depth in Token Vault: Mint vs Broker.

Three tables matter:

  • resources — one row per downstream system. Columns: slug, backend_kind, aud (audience URI), scopes (the catalog), policy (exchange allowlist), broker_provider_id (for Broker), fronting_link (for Fronted).
  • broker_providers — registration of an upstream OAuth provider. One Broker resource references one provider via broker_provider_id.
  • consent_grants — per-(user, agent, resource) consent attestations. Required before AuthPlane will issue a token for any (user, client, resource) tuple.

For Broker resources, a fourth table — broker_grants — records the per-(user, broker_provider) upstream OAuth grant: the encrypted refresh-grant AuthPlane uses to mint fresh upstream access tokens.

A scope is a string in the token’s scope claim. Scopes are declared on the resource:

resources:
- slug: mcp-server-prod
backend_kind: mint
scopes:
- { name: mcp:echo }
- { name: mcp:query_database }

Naming convention. Scope names are arbitrary strings — AuthPlane doesn’t parse them. The docs and examples use mcp:<tool_name> to match OAuth scope conventions (read:user, write:repo). Pick a convention and apply it consistently: the string you put in Resource.scopes is the same string you’ll write in @require_scopes(...) on the tool handler and the same string the client asks for on /oauth/token.

For Broker resources, scopes have an extra dimension — an upstream mapping from the AuthPlane-side scope name to the upstream provider’s scopes:

resources:
- slug: github
backend_kind: broker
broker_provider_slug: github
scopes:
- { name: repo, upstream: [repo] }
- { name: read:user, upstream: [read:user] }

This indirection lets you publish a stable AuthPlane-side scope catalog even when the upstream provider’s scopes change.

Per-tool scopes: the four places one scope name appears

Section titled “Per-tool scopes: the four places one scope name appears”

A single scope string (e.g. mcp:echo) shows up in four places. They must agree, and each side is enforced independently. Skim this once and you’ll never get a confusing 401 from a scope mismatch again.

#WhereWho declares itWhat happens if it doesn’t match
1Resource.scopes[].name in config or POST /admin/resourcesThe operator, once per resourceA scope not listed here is not issuable — AuthPlane strips it from token requests.
2scope=... form param on POST /oauth/tokenThe OAuth client, per requestAsking for a scope the resource doesn’t declare → rejected with invalid_scope.
3scope claim in the issued JWTAuthPlane, automaticallyThis is what your server reads. It’s the intersection of what the client asked for and what the resource declares.
4require_scopes("mcp:echo") on the tool handler (Python), RequireScope middleware (Go), requireScope (TypeScript)The MCP server builder, per toolA token missing the required scope → 403 insufficient_scope with the missing scope name in the body.

Clients name the target resource in their requests via the resource parameter, defined by RFC 8707:

POST /oauth/token
grant_type=authorization_code
code=...
resource=mcp-server-prod

AuthPlane looks up resources.slug = 'mcp-server-prod', sets the aud claim, and dispatches to the right issuer (Mint vs Broker). For token exchange, the same resource parameter selects the target. The indicator can name the resource by URI or by slug.

Once a token is minted, the aud claim names which resource(s) it’s for. Your MCP server MUST reject any token whose aud doesn’t include its own URI. Audience binding is what stops a token issued for resource A from being replayed against resource B.

Matching is exact-string membership: for multi-audience tokens (one client, several MCP servers), aud is an array, and the resource server accepts the token if and only if its own URI appears, as-is, anywhere in that array.

When a token exchange targets a Broker resource, AuthPlane enforces three bounds before vending the upstream token:

  1. requested scopes ⊆ consent_grants.scopes (per-agent attestation)
  2. broker_grants.scopes_granted (per-provider grant)
  3. The acting client must satisfy resources.policy.exchange.allowed_client_ids (an empty list allows any consented client).

If any bound fails, AuthPlane returns a consent_required error with the appropriate consent_url and cause — see the failure-mode tables in Token Vault: Mint vs Broker.