Token Vault: GitHub
This page walks the full Token Vault loop for GitHub: create a GitHub OAuth app, register it as a broker provider, expose it through a Broker resource, have a user consent once via /connect/github, then exchange your MCP server’s token for the user’s real GitHub token (gho_…). Read the Token Vault overview first for the concepts and prerequisites — encryption at rest (data_encryption.driver) and token_exchange.enabled: true are required.
-
Create the GitHub OAuth app.
Go to
https://github.com/settings/developers→ OAuth Apps → New. Set the Authorization callback URL to:https://<your-authplane>/connect/github/callbackSave the
client_idandclient_secretGitHub returns. Typical scopes for MCP workloads:repo,read:user,read:org. -
Register the broker provider.
The client secret lives in an environment variable on the AuthPlane host — the server reads it by name, so it never appears in config files:
Terminal window export CONNECTOR_GITHUB_SECRET="<client secret from step 1>"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 step 1>","client_secret_env": "CONNECTOR_GITHUB_SECRET","authorize_url": "https://github.com/login/oauth/authorize","token_url": "https://github.com/login/oauth/access_token"}}'CLI equivalent:
authserver admin provider create --slug github --protocol oauth --config-data '{"client_id":"...","client_secret_env":"CONNECTOR_GITHUB_SECRET","authorize_url":"...","token_url":"..."}'. -
Create the Broker resource with scope mapping.
The resource names the AuthPlane-side scopes your MCP server may request and maps each to the upstream GitHub scope:
Terminal window 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"]}}}'policy.exchange.allowed_client_idslocks the resource down to your MCP server’s client ID. Leave the list empty to allow any consented client. -
Register your MCP server as a token-exchange client.
Token exchange is confidential-client only — public clients cannot exchange:
Terminal window curl -X POST http://localhost:9000/oauth/register \-H "Content-Type: application/json" \-d '{"client_name": "MCP Server (prod)","redirect_uris": ["http://localhost:9999/callback"],"grant_types": ["urn:ietf:params:oauth:grant-type:token-exchange"],"token_endpoint_auth_method": "client_secret_post"}'If the client already exists, add the grant type via
PATCH /admin/clients/{id}. -
User consent via
/connect/github.Each user connects GitHub once. Direct their browser to the Connect endpoint with a
return_urlfrom yourconnect.allowed_return_urls:GET /connect/github?resource=github&return_url=https://app.example.com/connectedAuthPlane redirects to GitHub’s authorize page with HMAC-signed state; the user approves the scopes; GitHub redirects to
/connect/github/callback; AuthPlane exchanges the code, encrypts the refresh grant, writes abroker_grantsrow keyed(user_id, broker_provider_id), and redirects toreturn_url. Confirm it landed:Terminal window curl -H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY" \http://localhost:9001/admin/users/<user-id>/grants -
Exchange for a GitHub token.
From your MCP server, forward the user’s AuthPlane-issued access token as
subject_tokenand name the Broker resource:Terminal window 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 is the user’s actual GitHub access token — not an AuthPlane-signed JWT:
{"access_token": "gho_xxxxxxxxxxxx","issued_token_type": "urn:ietf:params:oauth:token-type:access_token","token_type": "Bearer","expires_in": 3600,"scope": "repo"}If the upstream access token has expired, AuthPlane refreshes it transparently using the encrypted refresh grant. Note that
resource=is the resource slug (github), not a URL.
Verify
Section titled “Verify”Use the vended token against the real GitHub API:
curl -H "Authorization: Bearer gho_xxxxxxxxxxxx" https://api.github.com/userA 200 with the user’s GitHub profile confirms the round-trip.
Handle consent_required in your MCP server
Section titled “Handle consent_required in your MCP server”When any of the three consent bounds is missing, the token endpoint returns HTTP 400 with {"error":"consent_required","consent_url":...}. Redirect the user to consent_url and retry the exchange when they return — AuthPlane picks the right URL automatically:
- No broker grant on file (the user never ran the Connect flow, or revoked it) →
consent_urlpoints at/connect/github. - No per-resource consent for the agent (the MCP client never ran a standard OAuth 2.1 authorize flow with
resource=github) →consent_urlpoints at/authorize?resource=.... - The upstream granted a strict subset of the requested scopes → re-run
/connect/githubto widen scopes.
The AuthPlane SDKs surface this as a typed error — for example the Python SDK raises ConsentRequiredError with a .consent_url field. See the runnable broker example at examples/python/04-broker-upstream.
Revocation and audit
Section titled “Revocation and audit”| Operation | Endpoint |
|---|---|
| User lists their connected providers | GET /connections |
| User disconnects GitHub | 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} |
Every token issuance is logged in the issuances table (including the DPoP key thumbprint when DPoP is in use) — inspect via GET /admin/issuances. If the user revokes your OAuth app on the GitHub side, the next exchange surfaces as consent_required — direct them back through /connect/github.