Skip to content

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.

  1. 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/callback

    Save the client_id and client_secret GitHub returns. Typical scopes for MCP workloads: repo, read:user, read:org.

  2. 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":"..."}'.

  3. 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_ids locks the resource down to your MCP server’s client ID. Leave the list empty to allow any consented client.

  4. 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}.

  5. User consent via /connect/github.

    Each user connects GitHub once. Direct their browser to the Connect endpoint with a return_url from your connect.allowed_return_urls:

    GET /connect/github?resource=github&return_url=https://app.example.com/connected

    AuthPlane 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 a broker_grants row keyed (user_id, broker_provider_id), and redirects to return_url. Confirm it landed:

    Terminal window
    curl -H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY" \
    http://localhost:9001/admin/users/<user-id>/grants
  6. Exchange for a GitHub token.

    From your MCP server, forward the user’s AuthPlane-issued access token as subject_token and 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.

Use the vended token against the real GitHub API:

Terminal window
curl -H "Authorization: Bearer gho_xxxxxxxxxxxx" https://api.github.com/user

A 200 with the user’s GitHub profile confirms the round-trip.

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_url points 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_url points at /authorize?resource=....
  • The upstream granted a strict subset of the requested scopes → re-run /connect/github to 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.

OperationEndpoint
User lists their connected providersGET /connections
User disconnects GitHubDELETE /connections/{provider}
Operator lists a user’s grantsGET /admin/users/{id}/grants
Operator revokes one broker grantDELETE /admin/grants/broker/{id}
Operator revokes one consent grantDELETE /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.