Skip to content

OAuth2 Grants and Tokens

Chenglei Yuan edited this page Jun 4, 2026 · 1 revision

OAuth2 Grants and Tokens

This page explains the two supported OAuth2 grants, the interactive browser login, and how access and refresh tokens are managed at runtime. For the underlying module internals, see OAuth2 Internals.

Choosing a grant

Grant When to use Human in the loop?
client_credentials Service accounts, CI, headless automation. No
authorization_code End users acting as themselves, with consent. Yes (first run)

The grant is selected by oauth2.grant. A token endpoint (tokenUrl) is required for either grant — supply it directly or let Discovery find it.

client_credentials

A single POST to the token endpoint:

grant_type=client_credentials
scope=<scope>            (if set)
audience=<audience>      (if set)
<extraParams…>

Client authentication follows authStyle:

  • "body" (default) — client_id / client_secret in the form body.
  • "header" — HTTP Basic Authorization header, with client_id also in the body.

There is no refresh token; the proxy simply re-runs the grant when the access token nears expiry.

authorization_code + PKCE

This grant supports three input modes, tried in order:

  1. Pre-existing refresh token (oauth2.refreshToken, or a cached one) — the proxy goes straight to refreshing; no browser.
  2. Pre-supplied one-shot code (oauth2.authorizationCode + oauth2.codeVerifier) — exchanged exactly once.
  3. Interactive browser flow — the default for first-time human login.

The interactive flow, step by step

  1. Generate a PKCE verifier (32 random bytes, base64url) and its S256 challenge.
  2. Generate a random state value (CSRF protection).
  3. Start a local HTTP listener on callbackHost:callbackPort (default 127.0.0.1:53682), path /callback.
  4. Build the authorize URL (response_type=code, code_challenge_method=S256, state, scope, redirect URI, any extraParams) and open it in your default browser. The URL is also printed to stderr so you can open it manually (handy over SSH — see Remote Hosts (SSH Port Forwarding)).
  5. The IdP redirects back to the local listener with code + state. The proxy validates state, then closes the listener.
  6. Exchange the code (plus PKCE verifier) at the token endpoint.
  7. If the response contains a refresh token, persist it (encrypted) and switch to the refresh-token grant for all future renewals.

The callback listener enforces a loopback-only Host header to defend against DNS-rebinding attacks, and warns if you bind it to a non-loopback address. See Security.

Refresh-token rotation

When a refresh produces a new refresh token (rotating IdPs), the proxy updates the in-memory token and rewrites the encrypted cache so the next launch uses the latest one.

Token lifecycle at runtime

A single TokenManager owns the access token for the process:

  • Caching. The current access token and its expiresAt are kept in memory.
  • Proactive refresh. When a request needs a token and the cached one is within the skew window (refreshSkewSeconds, default 30s) of expiry, it is refreshed first.
  • In-flight de-duplication. Concurrent callers awaiting a refresh share a single in-flight request rather than stampeding the token endpoint.
  • 401 invalidation + retry. If the upstream returns 401, the bridge calls invalidate() to drop the cached token, fetches a fresh one, and retries the request once. A second 401 is surfaced to the client. See Bridge Internals.

If expires_in is missing from a token response, it defaults to 3600 seconds; token_type defaults to Bearer.

Refresh-token cache

For the authorization_code grant, the refresh token is cached on disk so you only log in once per machine.

  • Encryption: AES-256-GCM. The file layout is iv(12) ‖ tag(16) ‖ ciphertext. A random 32-byte key is generated once and stored next to the cache as key.bin with 0600 permissions.

  • File name: derived from a SHA-256 of clientId|tokenUrl (first 16 hex chars) so different clients/endpoints don't collide.

  • Atomic writes: written to a temp file then renamed.

  • Default location (override with OAUTH2_TOKEN_CACHE_DIR):

    OS Path
    Windows %APPDATA%\mcp-oauth2-proxy\
    macOS ~/Library/Application Support/mcp-oauth2-proxy/
    Linux ${XDG_CONFIG_HOME:-~/.config}/mcp-oauth2-proxy/

A corrupt or unreadable cache is ignored (with a warning) and the proxy falls back to interactive login.

See Security for the threat model around the cache, and OAuth2 Internals for the implementation.

Clone this wiki locally