Add PKCE and device flow OAuth support for stdio mode, enabling browser-based authentication as an alternative to PATs. Flow priority (security-ordered): 1. PKCE + browser auto-open (native) 2. PKCE + URL elicitation (Docker with bound port) 3. Device flow fallback (more phishable, last resort) Key changes: - internal/oauth: self-contained OAuth manager with PKCE and device flow - internal/buildinfo: build-time OAuth credential injection via ldflags - BearerAuthTransport: added TokenProvider for dynamic token resolution - OAuth middleware intercepts tools/call to trigger lazy authentication - Scope-based tool filtering using existing SupportedScopes - PAT remains optional when OAuth credentials are configured Security: - PKCE S256 prevents code interception - State parameter prevents CSRF - Callback binds to 127.0.0.1 only - URL elicitation for sensitive URLs (never exposed to LLM) - Tokens stored in memory only, never persisted to disk - ReadHeaderTimeout prevents Slowloris on callback server - html/template auto-escaping prevents XSS in callback pages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5.4 KiB
OAuth Authentication (stdio mode)
The GitHub MCP Server supports OAuth 2.1 authentication for stdio mode, allowing users to authenticate via their browser instead of manually creating Personal Access Tokens.
How It Works
When no GITHUB_PERSONAL_ACCESS_TOKEN is configured and OAuth credentials are available, the server starts without a token. On the first tool call, it triggers the OAuth flow:
-
PKCE flow (primary): A local callback server starts, your browser opens to GitHub's authorization page, and the token is received via callback. If the browser cannot open (e.g., Docker), the authorization URL is shown via MCP URL elicitation.
-
Device flow (fallback): If the callback server cannot start (e.g., Docker without port binding), the server falls back to GitHub's device flow. A code is displayed that you enter at github.com/login/device.
Authentication Priority
| Priority | Source | Notes |
|---|---|---|
| 1 (highest) | GITHUB_PERSONAL_ACCESS_TOKEN |
PAT is used directly, OAuth is skipped |
| 2 | GITHUB_OAUTH_CLIENT_ID (env/flag) |
Explicit OAuth credentials |
| 3 | Built-in credentials | Baked into official releases via build flags |
Docker Setup (Recommended)
Docker is the standard distribution method. The recommended setup uses PKCE with a bound port:
{
"mcpServers": {
"github": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "GITHUB_OAUTH_CLIENT_ID",
"-e", "GITHUB_OAUTH_CLIENT_SECRET",
"-e", "GITHUB_OAUTH_CALLBACK_PORT=8085",
"-p", "127.0.0.1:8085:8085",
"ghcr.io/github/github-mcp-server"
],
"env": {
"GITHUB_OAUTH_CLIENT_ID": "your_client_id",
"GITHUB_OAUTH_CLIENT_SECRET": "your_client_secret"
}
}
}
}
Security: Always bind to
127.0.0.1(not0.0.0.0) to restrict the callback to localhost.
Docker Without Port Binding (Device Flow)
If you cannot bind a port, the server falls back to device flow:
{
"mcpServers": {
"github": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "GITHUB_OAUTH_CLIENT_ID",
"-e", "GITHUB_OAUTH_CLIENT_SECRET",
"ghcr.io/github/github-mcp-server"
],
"env": {
"GITHUB_OAUTH_CLIENT_ID": "your_client_id",
"GITHUB_OAUTH_CLIENT_SECRET": "your_client_secret"
}
}
}
}
Native Binary Setup
For native binaries, PKCE works automatically with a random port:
export GITHUB_OAUTH_CLIENT_ID="your_client_id"
export GITHUB_OAUTH_CLIENT_SECRET="your_client_secret"
./github-mcp-server stdio
The browser opens automatically. No port configuration needed.
Creating a GitHub OAuth App
- Go to GitHub Settings → Developer settings → OAuth Apps
- Click New OAuth App
- Fill in:
- Application name: e.g., "GitHub MCP Server"
- Homepage URL:
https://github.com/github/github-mcp-server - Authorization callback URL:
http://localhost:8085/callback(match your--oauth-callback-port)
- Click Register application
- Copy the Client ID and generate a Client Secret
Note
: The callback URL must be registered even for device flow, though it won't be used.
Configuration Reference
| Environment Variable | Flag | Description |
|---|---|---|
GITHUB_OAUTH_CLIENT_ID |
--oauth-client-id |
OAuth client ID |
GITHUB_OAUTH_CLIENT_SECRET |
--oauth-client-secret |
OAuth client secret |
GITHUB_OAUTH_CALLBACK_PORT |
--oauth-callback-port |
Fixed callback port (0 = random) |
GITHUB_OAUTH_SCOPES |
--oauth-scopes |
Override automatic scope selection |
Security Design
PKCE (Proof Key for Code Exchange)
All authorization code flows use PKCE with S256 challenge, preventing authorization code interception even if an attacker can observe the callback.
Fixed Port Considerations
Docker requires a fixed callback port for port mapping. This is acceptable because:
- PKCE verifier is generated per-flow and never leaves the process — an attacker who intercepts the callback cannot exchange the code
- State parameter prevents CSRF — the callback validates state match
- Callback server binds to 127.0.0.1 — not accessible from outside the host
- Short-lived — the server shuts down immediately after receiving the callback
Token Handling
- Tokens are stored in memory only — never written to disk
- OAuth token takes precedence over PAT if both become available
- The server requests only the scopes needed by the configured tools
URL Elicitation Security
When the browser cannot auto-open, the authorization URL is shown via MCP URL-mode elicitation. This is secure because:
- URL elicitation presents the URL to the user without exposing it to the LLM context
- The MCP client shows the full URL for user inspection before navigation
- Credentials flow directly between the user's browser and GitHub — never through the MCP channel
Device Flow as Fallback
Device flow is more susceptible to social engineering than PKCE (the device code could theoretically be phished), which is why PKCE is always attempted first. Device flow is only used when a callback server cannot be started.