# Authentication, tokens and revocation

Step 4 reads what the identity can do; step 7 checks where its credential lives and who can switch it off. This file covers both. Vendor menu paths change: verify each one live before telling a user where to click.

## Auth methods, best first for marketing work

1. Per-user OAuth with PKCE to a vendor-hosted server. Each person consents, vendor logs show the real actor, and deactivating the vendor account ends access. HubSpot's remote server requires PKCE.
2. OAuth through a proxy server, such as a hosted community server or an aggregator. The proxy decides which upstream scopes to request; your client's scope settings cannot narrow them.
3. Service accounts and system user tokens (Google Cloud service accounts, Meta system users, Amplitude service accounts). Needed for unattended runs. They need a narrow role, a rotation owner and an actor log outside the agent that records who asked for each call (C9).
4. Long-lived API keys and master keys. No consent moment, they outlive people, and they reach new tools as soon as the vendor ships them: Apollo's headless MCP route needs a master key. Apollo's API docs also say key-based requests act as the workspace's longest-standing active admin while OAuth acts as the signed-in user, so a key created by a junior rep does not carry that rep's narrower access. Verify both live.
5. Keys in URLs: legacy SSE endpoints with the key as a query parameter, or a URL-path fallback for clients that cannot send headers. Treat the whole URL as a secret (C14) and use the header route wherever the client supports it.

## Reading a consent screen

- List every scope on the screen and compare it with the task. Record the list in the permissions section of the record.
- When a server's authorization challenge names no scope, the MCP guidance has the client request everything the server lists in `scopes_supported`, so the screen can show the whole catalogue. The MCP security best practices name publishing every scope and omnibus scopes as common mistakes.
- Proxy traps:
  - Stape's hosted GTM endpoint requests all seven Tag Manager scopes (manage.accounts, edit.containers, delete.containers, edit.containerversions, manage.users, publish, readonly) plus email and profile (its scopes.ts, last changed 2026-08-14). The GTM container permission is then the only server-side boundary: give the identity Read on one container, or audit from an export instead.
  - Turning off the Google Ads MCP server's write categories leaves its credential as it was: the documented credential uses the Google Ads API scope, which also allows writes at the API, plus cloud-platform in Application Default Credentials. Any local script using the same credentials inherits that authority. Use a dedicated identity (service account impersonation is documented) with the Google Ads Read-only access level.
  - The GA4 MCP README's credential requests analytics.readonly plus cloud-platform; version 1.0.0's three write tools add analytics.edit, through `--allow-write` or `ANALYTICS_MCP_ALLOW_WRITE`.
- HubSpot: scopes follow the tools available at install plus the user's grant. When tools change, users must re-install to grant new scopes. Treat a re-install prompt as a re-review trigger.

## Hidden is not revoked

- Client-side scope pinning (Claude Code's `oauth.scopes`, for example) narrows only what the client requests from the MCP server's own authorization server. It cannot narrow an upstream grant a proxy requests from Google, Meta or a CRM.
- Switches that hide write tools leave the credential's authority intact. Windsor.ai's `include_actions=False` only skips listing write actions. A community Search Console server's write-disable flag removes its write tools, but its auth code still accepts an older token that holds the read-write scope.
- Fix: revoke the broad grant, sign in again with the narrow scope, then prove it with a denied test write.

## Where tokens are stored

Verify each client's current behaviour live; record what you found per client in the record.

- OS keychains (macOS Keychain, Windows Credential Manager, libsecret) are the target state.
- Claude Code keeps OAuth tokens in the macOS Keychain, and in a `.credentials.json` file with 0600 permissions on Linux and Windows or when the keychain is unavailable.
- MCP Inspector persists acquired OAuth tokens by default, in the OS keychain or, without one, in an unencrypted `~/.mcp-inspector/secrets.json`. For review captures set `MCP_INSPECTOR_PERSIST_TOKENS=none` and `MCP_INSPECTOR_SECRET_STORE=memory`. Headers typed into its config are saved as written. Its `servers/show` method redacts env values and sensitive headers but not credentials inside a URL or stdio arguments.
- mcp-remote keeps credentials in `~/.mcp-auth` (or `MCP_REMOTE_CONFIG_DIR`); its debug mode writes auth details there too.
- JSON config `env` blocks, `docker -e NAME=value` flags, `--header` arguments, connection strings with a password and shell profile exports are plaintext (CF01, CF02, C8). So is a client secret in an `oauth` table: Codex saves `mcp_servers.<name>.oauth.client_secret` in `config.toml` as written, and a hand-written Inspector catalog can hold `oauth.clientSecret` in the file.
- Any process running as the same user can read a 0600 file. An unreviewed local server running as you can read every plaintext token store on the machine, which is why C2 isolation matters.

## One credential per server and per person

- No global shell exports. Use references the scanner recognises: `${VAR}`, `${env:VAR}`, `op://` secret references, VS Code input variables, Codex `bearer_token_env_var` and `env_http_headers` (which hold variable names, not values).
- Claude Code reads only its own credential variables (for example `ANTHROPIC_API_KEY`), cloud provider credentials (for example `AWS_BEARER_TOKEN_BEDROCK`) and a few others (for example `NPM_TOKEN`) as empty in a remote server's URL and headers. Any other name, such as `HUBSPOT_ACCESS_TOKEN` or `API_KEY`, expands as written, so an approved project `.mcp.json` can send an exported marketing token to a server it names. Keep marketing tokens out of global shell exports and read every project server's URL and headers before approving it. Verify the covered list live.
- Long-lived keys: a dedicated key per connection, the narrowest key type (a Klaviyo private key with read-only scopes; a PostHog personal API key with the MCP Server preset, scoped to one project), a rotation owner, rotation every 90 days by default, and immediate rotation on any exposure: committed, pasted, screenshotted, attached to a support ticket.
- Never in prompts, chat, memory files (CLAUDE.md, AGENTS.md and similar) or screenshots.

## Revocation map

- Client first, then vendor. Removing a remote server in Claude Code deletes its local tokens but does not revoke them at the authorization server; run `claude mcp logout <name>` before `claude mcp remove`. Codex signs in with `codex mcp login <name>`; check its logout command live.
- Vendor connected-apps or OAuth settings revoke one grant. Verify the menu path live for each vendor.
- Company kill switches:
  - Braze company OAuth settings can turn off MCP OAuth and revoke every MCP OAuth token at once. It cannot be undone; write that into the incident plan.
  - Customer.io's admin toggle (Settings, AI) stops MCP requests immediately, but turning it back on restores the same sessions because the tokens are kept. It is a pause, not a revocation.
- Per-user only: in Customer.io, only the user who created an MCP session can revoke it, from their personal settings (Connected clients, View sessions). Admins cannot view or revoke other users' sessions, so the offboarding checklist needs the user, or the account deactivated.
- API keys: delete or rotate the key at the vendor. Hiding it in the client does nothing.
- Google Cloud: remove the IAM binding or the service account key; IAM deny policies also apply to MCP tools.

## Revocation test (C11)

1. With the test identity, run one allowed read. Record the time.
2. Revoke at the vendor, and log out in the client.
3. Wait any delay the vendor documents.
4. Call the same tool once more. Pass: an authentication or authorization error. Record the time and the error code, never the token.
5. Re-grant only if the review continues.

## Offboarding and hand-over

- OAuth grants end when the vendor account is deactivated or the grant is revoked, not when someone leaves the company directory.
- API keys survive people. Rotate every key a leaver could have seen.
- Agency hand-over: the receiving owner reconnects under their own identity. Never copy tokens or configs between organisations.

## Questions for builders of custom or internal servers

The full checklist is in custom-server-checks.md. The protocol requirements to ask about: tokens validated for this server's audience, no token passthrough to upstream APIs, PKCE with S256, HTTPS endpoints, short-lived access tokens, refresh token rotation for public clients, and the 2026-07-28 issuer rules (an authorization server includes `iss` in authorization responses per RFC 9207; a client validates it and keys stored client credentials by issuer).
