Token Vault
TL;DR — When your MCP server needs to call GitHub / Slack / Google / Linear on a user’s behalf, the naïve approach is to store each user’s refresh token per MCP. AuthPlane centralizes this: the user connects each upstream provider once via the Connect flow, AuthPlane encrypts the refresh grant, and any authorized MCP server vends a short-lived upstream token on demand via RFC 8693 token exchange. Three-bound consent gates every vend. “Token Vault” is the mental name; the implementation is the unified Resource model with
backend_kind: broker—mintresources are AS-signed,brokerresources vend upstream tokens.
The problem it solves
Your MCP server “GitHub PR reviewer” needs to make GitHub API calls on the user’s behalf. Without central credential management:
- Every MCP server implements its own OAuth flow with each upstream.
- Every MCP stores refresh tokens somewhere (usually badly — plaintext DB, leaked logs).
- When a user revokes access at GitHub, every MCP is a stale reference.
- Auditing “which agent called what upstream on my behalf” is spread across N systems.
The Token Vault pattern moves credential storage + refresh + audit to one place — AuthPlane. Every MCP server calls one endpoint (/oauth/token with token-exchange grant), gets a fresh short-lived upstream token, forwards it, done.
Mental model — Mint vs Broker
Every AuthPlane resource has a backend_kind:
mint— AuthPlane signs its own JWT for the resource. Standard OAuth resource server.broker— AuthPlane vends a token minted by an upstream provider. AuthPlane doesn’t sign the vended token — GitHub does. But AuthPlane holds the stored credential + orchestrates consent + audits the vend.
broker resources have a broker_provider_slug pointing at a registered upstream OAuth provider. The provider’s config includes the OAuth client credentials AuthPlane uses when running the Connect flow with the upstream.
The full picture
flowchart TD
Connect(["Connect flow (one-time)"])
AuthPlane["AuthPlane<br/>broker_grants<br/>per-user<br/>encrypted at rest<br/>(AES-GCM or Vault Transit)"]
MCP["Your MCP Server<br/>(tool handler)"]
IdP["Upstream IdP<br/>(GitHub)"]
API["Upstream API<br/>(github.com)"]
Connect -->|writes here| AuthPlane
AuthPlane -->|exchange call| MCP
IdP -->|upstream OAuth token endpoint| AuthPlane
MCP ---|"upstream bearer (Bearer gh_xxx)"| IdP
MCP --> API
Two flows:
- Connect flow (one-time per user × provider) — user browses to
/connect/github, approves at GitHub, AuthPlane stores the encrypted refresh grant inbroker_grants. Never plaintext on disk. - Vend flow (per request) — your MCP calls
/oauth/tokenwith token-exchange grant naming a broker resource. AuthPlane checks the three-bound consent, uses the stored refresh grant to get a fresh upstream token, returns it. Your MCP forwards it.
The three-bound consent model
Every vend is gated by three bounds:
requested_scopes ⊆ consent_grants.scopes (per-agent attestation)
⊆ broker_grants.scopes_granted (per-provider grant)
Concretely, five checks:
The cause sub-discriminator (consent_missing vs scope_insufficient) tells the SDK which consent_url to attach. All of these translate to MCP JSON-RPC -32042 UrlElicitationRequiredError in your tool code — your handler doesn’t need try/except.
Vending is never cached
Every /oauth/token for a broker resource hits the upstream IdP (or the stored refresh grant if the current AT is still fresh). AuthPlane doesn’t hold upstream access tokens in memory — the vended token flows through and is your MCP’s responsibility until it expires.
Concurrent vends are serialized via optimistic locking. Two simultaneous vends that both need refresh → the second gets HTTP 423 Locked. SDK clients retry once on 423.
What happens on upstream revocation
If the user revokes AuthPlane’s OAuth app at GitHub, the next refresh attempt gets rejected. AuthPlane responds 400 consent_required cause=consent_missing consent_url=/connect/github. Client walks the user through re-connecting; the encrypted broker_grants row is replaced.
Why encrypted at rest
The data_encryption driver (AES-256-GCM or Vault Transit) is a hard requirement when broker_providers are configured — boot fails otherwise. Stored plaintext refresh grants are a compliance and blast-radius nightmare; AuthPlane refuses to run without encryption.
aes_master— local AES-256-GCM with HKDF-derived per-purpose subkeys. Master key via env var (AUTHPLANE_DATA_ENCRYPTION_KEY).vault_transit_encrypt— plaintext never touches the AuthPlane process; delegated to Vault Transit.
Retirement of “Token Vault” terminology
Historically this was called “Token Vault”. The name was retired in v0.1.0-rc1 in favor of the unified Resource model — same on the wire, cleaner mental model. The broker_grants table name reflects the current terminology; the vault_transit_* driver names refer to HashiCorp Vault Transit (an external KMS) and are unrelated.
Related
- Guides: Wire up the Token Vault — end-to-end setup walkthrough
- Guides: Upstream connections — per-protocol broker provider setup
- Topologies: Agent + brokered MCP — the topology diagram
- Concepts: Grants & flows → Token exchange
- Reference: Configuration → broker_providers + connect
- Reference: Errors → Broker/Vault