Skip to content

Token Vault: Mint vs Broker

AuthPlane answers one question per resource: who signs the access token it vends?

  • A Mint resource is a resource AuthPlane mints tokens for. AuthPlane is the issuer; the token is an at+jwt JWT signed by AuthPlane’s JWKS; your MCP server validates it offline.
  • A Broker resource is backed by a third-party provider — GitHub, Google, Slack, Notion, Linear, Atlassian, or any generic OAuth 2.0 upstream. AuthPlane delegates token minting to that provider: it stores the user’s upstream credential encrypted at rest and vends the provider’s native token (e.g. a GitHub gho_…) fresh on every request via RFC 8693 token exchange.
MintBroker (Token Vault)
Who signs the token?AuthPlaneThe upstream provider
Token formatAuthPlane-signed JWT (at+jwt)The provider’s native token (e.g. gho_… for GitHub; opaque elsewhere)
What the resource server validates againstAuthPlane’s JWKSThe provider’s API
Does AuthPlane hold provider credentials?NoYes — the upstream OAuth refresh token, encrypted at rest in broker_grants
Who consents?The user consents once for the agent on this resourceThe user consents on the agent and connects the provider
Typical useYour MCP serverCalling third-party APIs as the user

In the Mint case, the resource server never talks to a third party — it trusts AuthPlane’s signature. In the Broker case, AuthPlane is the OAuth broker: it holds the user’s encrypted refresh token, talks to the provider, and hands the agent a token the provider itself minted.

Section titled “How the vault fills: the /connect/{provider} consent flow”

Broker has two phases: a one-time per-user setup, then a per-call vend.

  1. Direct the user’s browser to GET /connect/{provider} with a return_url listed in your connect.allowed_return_urls:

    GET /connect/github?resource=github&return_url=https://app.example.com/connected
  2. AuthPlane redirects the browser to the provider’s authorize page with HMAC-signed state (the HMAC key comes from AUTHPLANE_CONNECT_STATE_SECRET — required, it prevents CSRF in the Connect flow).

  3. The user approves the scopes at the provider.

  4. The provider redirects back to /connect/{provider}/callback. AuthPlane exchanges the code, then encrypts and stores the refresh-grant in broker_grants.

How the vault vends: token exchange (RFC 8693)

Section titled “How the vault vends: token exchange (RFC 8693)”

Every time an agent or MCP server needs a fresh upstream token, it calls POST /oauth/token with grant_type=urn:ietf:params:oauth:grant-type:token-exchange and resource=<broker-slug>. AuthPlane looks up the stored grant, refreshes the upstream access token with the provider, and returns it — in standard RFC 8693 response format, but the credential inside is the provider’s own token, not an AuthPlane JWT. (Requires token_exchange.enabled: true, or AUTHPLANE_TOKEN_EXCHANGE_ENABLED=true.)

Two consequences worth internalizing:

  1. Revocation is fast. When the user revokes the upstream OAuth grant at the provider, the next refresh fails — and the agent’s next call gets consent_required immediately. No long-lived stale tokens sitting in agent memory.
  2. The agent never sees the upstream refresh token. Only the per-call access token. That keeps the blast radius of a leaked agent-side credential bounded.

Plaintext refresh-grants never persist. The vault requires one of two data_encryption drivers (set via data_encryption.driver or AUTHPLANE_DATA_ENCRYPTION_DRIVER):

DriverHow it worksChoose it when
aes_masterAuthPlane encrypts with a 32-byte AES-256 master key (64 hex chars) read from an env var you name in data_encryption.aes_master.key_env. Supports rotation via an old_key_env decrypt-only fallback.Simple single-host or HA-Postgres deploys.
vault_transit_encryptEncryption is delegated to HashiCorp Vault’s Transit secrets engine — key material never leaves Vault. Token or AppRole auth.FIPS/HSM requirements, strict segregation of duties, multi-replica deployments.

See the upstream HashiCorp Vault Transit guide for the full recipe.

Each Broker resource declares who may vend from it via resources.policy.exchange.allowed_client_ids (an empty list allows any consented client). Before vending, AuthPlane enforces three bounds:

requested scopes ⊆ consent_grants.scopes (per-agent attestation) ⊆ broker_grants.scopes_granted (per-provider grant), and the acting client satisfies allowed_client_ids.

resources:
- slug: github
backend_kind: broker
broker_provider_slug: github
scopes:
- { name: repo, upstream: repo }
- { name: read:user, upstream: read:user }
policy:
exchange:
allowed_client_ids: ["mcp-server-prod"] # only the MCP server may vend

If any bound fails, POST /oauth/token returns consent_required with a cause (consent_missing or scope_insufficient) and a consent_url. For agent-side gaps the URL points at AuthPlane’s /authorize; for provider-side gaps it points at /connect/{provider} so the user can (re-)connect. Clients that don’t want to branch can simply open consent_url in all cases.

A common combination: a Mint MCP server with its own local tools that also needs a vaulted provider token (say, a tools/summarize_my_prs tool needing GitHub). A fronting linksource_slug (the Mint), target_slug (the Broker), and a scope_map — authorizes the MCP server to exchange the agent’s Mint token for a Broker-vended one. The agent presents one token, the user consents once, and the agent never holds a GitHub token at all. Fronting links are admin-API-only (POST /admin/fronting on port 9001). Full walkthrough in the upstream Mint vs Broker doc.