-
Notifications
You must be signed in to change notification settings - Fork 1
OAuth2 Internals
A deep dive into the oauth2/ layer: the grant abstraction, token management,
discovery, the interactive callback server, and the encrypted token cache. For
user-facing behavior see OAuth2 Grants and Tokens and Discovery.
All grants implement a small interface (oauth2/grants/types.ts):
interface Grant {
readonly name: string;
fetchToken(): Promise<TokenResponse>;
supportsRenewal(): boolean;
}fetchToken() returns a normalized token (access token, expiresIn,
tokenType, optional refreshToken). supportsRenewal() tells the
TokenManager whether silent renewal is possible.
oauth2/tokenManager.ts is the single token authority:
- Caches the current access token plus computed
expiresAt. -
getToken()returns the cached token unless it is missing or within the skew window (refreshSkewSeconds), in which case it callsgrant.fetchToken(). -
In-flight de-duplication: concurrent
getToken()calls during a refresh share one promise (this.inflight) instead of each hitting the token endpoint. -
invalidate()clears the cache so the nextgetToken()forces a refresh — this is what the bridge calls on a401.
oauth2/factory.ts builds the configured grant:
- A
tokenUrlis mandatory —buildGrantthrows if none is configured or discovered. -
client_credentials→ClientCredentialsGrant. -
authorization_code→ constructs aTokenCache, loads any cached refresh token (an explicitcfg.refreshTokenwins over the cache), and wiresonRefreshTokenUpdatedto persist rotated refresh tokens back to the cache. - The factory accepts either an
OAuthConfigor a{ cfg, log }pair.
POSTs grant_type=client_credentials with optional scope, audience, and
extraParams. No refresh token; renewal re-runs the grant.
Lazily resolves a token via three modes (in order):
-
Pre-existing refresh token → immediately delegate to a
RefreshTokenGrant. - Pre-supplied one-shot code (+ PKCE verifier) → exchange exactly once.
- Interactive flow → run the browser login (below).
After a successful code exchange, if the response includes a refresh token it
fires onRefreshTokenUpdated and builds a RefreshTokenGrant delegate for all
future renewals. Reusing a code with no refresh token available throws.
POSTs grant_type=refresh_token. If the response returns a new refresh
token (rotation), it updates its in-memory value and fires
onRefreshTokenUpdated so the cache is rewritten.
-
undiciHttpClient.postForm(url, body, headers)—application/x-www-form-urlencodedrequest,Accept: application/json, 30 s timeouts. -
parseTokenResponse(status, bodyText)— throws on non-2xx, non-JSON, or missingaccess_token; defaultsexpires_into3600andtoken_typetoBearer. -
applyClientAuth(body, headers, opts)— forauthStyle: 'header', sets HTTP Basic auth (and still includesclient_idin the body); otherwise putsclient_id/client_secretin the body.
- Stage 1: fetch
/.well-known/oauth-protected-resource(RFC 9728), readauthorization_servers[0]. - Stage 2: fetch RFC 8414 (
/.well-known/oauth-authorization-server) or OIDC (/.well-known/openid-configuration) metadata, trying path-permutation candidates for issuers that carry a path. -
keepSecure()drops any discovered endpoint that is insecure cleartext (warns, doesn't throw). - 5-second per-request timeouts; every field of
DiscoveryResultis optional.
- PKCE: 32-byte random verifier → base64url; S256 challenge.
-
Authorize URL:
response_type=code,code_challenge_method=S256,state,scope, redirect URI, plus anyextraParams. Printed to stderr. -
Callback listener: binds
callbackHost:callbackPort, serves/callback.isAllowedHost()rejects non-loopbackHostheaders and port mismatches (DNS-rebinding defense). Thestatevalue is validated (CSRF). -
Browser open:
win32usesrundll32 url.dll,FileProtocolHandler(avoidscmd's&mangling in URLs);darwinusesopen; otherwisexdg-open. Spawn errors are swallowed (you can still open the printed URL manually). - Warns if
callbackHostis non-loopback.
-
Cipher: AES-256-GCM. File layout:
iv(12) ‖ tag(16) ‖ ciphertext. -
Key: random 32-byte
key.binstored alongside the cache with mode0600. -
File name:
<sha256(clientId|tokenUrl)[:16]>.json.enc. - Atomic writes: temp file + rename.
-
Default dir:
%APPDATA%\mcp-oauth2-proxy\(Windows),~/Library/Application Support/mcp-oauth2-proxy/(macOS),${XDG_CONFIG_HOME:-~/.config}/mcp-oauth2-proxy/(Linux). Override withOAUTH2_TOKEN_CACHE_DIR. - Load failures are ignored with a warning (fall back to interactive login).
See Security for the threat model around the cache and callback listener.
GitHub repo · npm package · Licensed under MIT
Overview
Guides
- Getting Started
- Configuration
- OAuth2 Grants and Tokens
- Discovery
- Security
- Remote Hosts (SSH Port Forwarding)
- Troubleshooting
Internals