Token Vault: Connect Upstream Providers
Token Vault is AuthPlane’s brokered upstream-OAuth capability. AuthPlane runs the OAuth dance with a third-party provider once per user, holds the resulting refresh grant encrypted at rest, and vends fresh upstream access tokens to your MCP servers on demand via RFC 8693 Token Exchange. Your MCP server gets the provider’s native token (e.g. gho_… for GitHub) — and never sees the refresh token, the most-stealable credential in the system.
In the configuration and admin API, Token Vault maps onto AuthPlane’s broker primitives:
| Token Vault concept | AuthPlane primitive |
|---|---|
| Provider connection (your OAuth app at GitHub, Google, …) | Broker provider — POST /admin/broker-providers or broker_providers: YAML seed |
| Scope catalog + access policy your MCP servers exchange against | Broker resource — a resource with backend_kind: broker |
| Per-user encrypted refresh grant, created by the consent flow | Broker grant — a broker_grants row keyed (user_id, broker_provider_id) |
Supported providers
Section titled “Supported providers”Any OAuth 2.0 provider that supports authorization_code + refresh_token works. These six have documented recipes; in each console, set the OAuth app’s callback URL to https://<your-authplane>/connect/<provider-slug>/callback:
| Provider | Console | Default slug | Required scope examples |
|---|---|---|---|
| GitHub | github.com/settings/developers → OAuth Apps → New | github | repo, read:user, read:org |
console.cloud.google.com/apis/credentials → OAuth client ID | google | https://www.googleapis.com/auth/calendar.readonly, openid email | |
| Slack | api.slack.com/apps → Create New App | slack | chat:write, channels:read |
| Notion | notion.so/my-integrations → New integration → OAuth public | notion | read_content, update_content |
| Linear | linear.app/settings/api/applications → Create new | linear | read, write |
| Atlassian | developer.atlassian.com/console/myapps → OAuth 2.0 (3LO) | atlassian | read:jira-work, write:jira-work, offline_access |
Each provider’s config_data block uses its own endpoints, plus a few quirks:
| Provider | authorize_url | token_url | Extra fields |
|---|---|---|---|
| GitHub | https://github.com/login/oauth/authorize | https://github.com/login/oauth/access_token | — |
https://accounts.google.com/o/oauth2/v2/auth | https://oauth2.googleapis.com/token | "extra_auth_params": {"access_type": "offline", "prompt": "consent"} | |
| Slack | https://slack.com/oauth/v2/authorize | https://slack.com/api/oauth.v2.access | "response_format": "slack" |
| Notion | https://api.notion.com/v1/oauth/authorize | https://api.notion.com/v1/oauth/token | — |
| Linear | https://linear.app/oauth/authorize | https://api.linear.app/oauth/token | — |
| Atlassian | https://auth.atlassian.com/authorize | https://auth.atlassian.com/oauth/token | "extra_auth_params": {"audience": "api.atlassian.com", "prompt": "consent"} |
Prerequisites
Section titled “Prerequisites”- Encryption at rest is required. Set
data_encryption.drivertoaes_masterorvault_transit_encrypt. The Connect flow silently disables itself if no data encryptor is configured — upstream refresh grants must be encrypted (AES-256-GCM or HashiCorp Vault Transit) before AuthPlane will store them. token_exchange.enabled: true(orAUTHPLANE_TOKEN_EXCHANGE_ENABLED=true).- An admin API key (
AUTHPLANE_ADMIN_API_KEY) and aconnect.state_secret(openssl rand -hex 32, min 32 chars) — the state secret signs the CSRF tokens binding the upstream callback to the user’s session.
Register a broker provider
Section titled “Register a broker provider”Put the client secret in an environment variable — AuthPlane reads it by name, so the value never appears in config files or API payloads:
export CONNECTOR_GITHUB_SECRET="<client secret from the provider console>"
curl -X POST http://localhost:9001/admin/broker-providers \ -H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "slug": "github", "display_name": "GitHub", "protocol": "oauth", "config_data": { "client_id": "<from the provider console>", "client_secret_env": "CONNECTOR_GITHUB_SECRET", "authorize_url": "https://github.com/login/oauth/authorize", "token_url": "https://github.com/login/oauth/access_token" } }'Or seed it declaratively in YAML:
broker_providers: - slug: github display_name: GitHub protocol: oauth config_data: client_id: "Iv1.test" client_secret_env: "CONNECTOR_GITHUB_SECRET" authorize_url: "https://github.com/login/oauth/authorize" token_url: "https://github.com/login/oauth/access_token"Expose it through a Broker resource
Section titled “Expose it through a Broker resource”A Broker resource names the fine-grained scopes AuthPlane may vend to MCP servers and the upstream scope each maps to:
curl -X POST http://localhost:9001/admin/resources \ -H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "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"] } } }'scopes[].name— the AuthPlane-side scope your MCP server requests (scope=repo).scopes[].upstream— the actual upstream scope sent to the provider on consent and used for the bound check.policy.exchange.allowed_client_ids— gates which MCP-server clients may vend this resource. An empty list means any consented client.
The /connect/{provider} consent flow
Section titled “The /connect/{provider} consent flow”Each user connects a provider once. Direct their browser to GET /connect/{provider} with a return_url listed in connect.allowed_return_urls:
GET /connect/github?resource=github&return_url=https://app.example.com/connected- AuthPlane redirects the browser to the provider’s authorize page with HMAC-signed state.
- The user approves the scopes at the provider.
- The provider redirects back to
/connect/github/callback. - AuthPlane exchanges the code, encrypts the refresh grant, writes a
broker_grantsrow keyed(user_id, broker_provider_id), and redirects toreturn_url.
Verify the grant landed with GET /admin/users/{id}/grants on the admin port.
Vend fresh upstream tokens via RFC 8693
Section titled “Vend fresh upstream tokens via RFC 8693”From your MCP server, trade the user’s AuthPlane-issued access token for the upstream token at POST /oauth/token:
curl -X POST http://localhost:9000/oauth/token \ -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ -d "subject_token=$USER_ACCESS_TOKEN" \ -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \ -d "resource=github" \ -d "scope=repo" \ -d "client_id=mcp-server-prod" \ -d "client_secret=$MCP_SERVER_SECRET"The response carries the user’s actual upstream access token (not an AuthPlane-signed JWT, since backend_kind=broker). Before responding, AuthPlane runs the three-bound check: requested scopes ⊆ the user’s consent grant for this agent and resource, requested scopes ⊆ broker_grants.scopes_granted for this user and provider, and the acting client ∈ policy.exchange.allowed_client_ids (or the list is empty). If the upstream access token has expired, AuthPlane refreshes it transparently using the encrypted refresh grant. If a required grant is missing, the token endpoint returns consent_required with a consent_url — redirect the user there and retry.
Disconnecting and revoking
Section titled “Disconnecting and revoking”| Operation | Endpoint |
|---|---|
| User lists their own connected providers | GET /connections |
| User disconnects a single provider | DELETE /connections/{provider} |
| Operator lists a user’s grants | GET /admin/users/{id}/grants |
| Operator revokes one broker grant | DELETE /admin/grants/broker/{id} |
| Operator revokes one consent grant | DELETE /admin/grants/consent/{id} |