Skip to content

Commit 0e7f331

Browse files
committed
feat(authentication): Disable the OAuth flow when running on HTTP mode
1 parent 02f3fe1 commit 0e7f331

File tree

17 files changed

+826
-341
lines changed

17 files changed

+826
-341
lines changed

DEVELOPMENT.md

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,18 +135,55 @@ for tool in example_tools:
135135
mcp.tool(tool)
136136
```
137137

138+
## Authentication Modes
139+
140+
The GitGuardian MCP server supports two authentication modes:
141+
142+
### 1. Local OAuth (stdio transport)
143+
For desktop applications using stdio transport, OAuth authentication is available:
144+
145+
```bash
146+
ENABLE_LOCAL_OAUTH=true developer-mcp-server
147+
```
148+
149+
This will:
150+
- Open a browser for OAuth authentication
151+
- Store the token locally in `~/.gitguardian/`
152+
- Reuse the token across sessions
153+
154+
### 2. Per-Request Authentication (HTTP/SSE transport)
155+
For server deployments using HTTP/SSE transport, use per-request PAT authentication:
156+
157+
```bash
158+
MCP_PORT=8080 MCP_HOST=127.0.0.1 developer-mcp-server
159+
```
160+
161+
Clients must provide authentication via the Authorization header:
162+
```
163+
Authorization: Bearer <your-personal-access-token>
164+
```
165+
166+
**Important:** You cannot use both modes simultaneously. The server will raise an error if both `MCP_PORT` and `ENABLE_LOCAL_OAUTH=true` are set.
167+
168+
### 3. Environment Variable PAT
169+
For all transport modes, you can provide a PAT via environment variable:
170+
171+
```bash
172+
GITGUARDIAN_PERSONAL_ACCESS_TOKEN=<your-pat> developer-mcp-server
173+
```
174+
138175
## Testing
139176

140-
Run tests using uv:
177+
Run tests using uv (OAuth is disabled by default in tests):
141178

142179
```bash
143-
uv run pytest
180+
ENABLE_LOCAL_OAUTH=false uv run pytest
144181
```
145182

146183
Run tests with verbose output:
147184

148185
```bash
149-
uv run pytest -v
186+
ENABLE_LOCAL_OAUTH=false uv run pytest -v
150187
```
151188

152189
Run tests with coverage:

README.md

Lines changed: 112 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,103 @@ To use the GitGuardian MCP server with [Windsurf](https://www.windsurf.ai/):
175175
}
176176
```
177177

178-
## Authentication Process
178+
## Authentication
179179

180-
1. When you start the server, it will automatically open a browser window to authenticate with GitGuardian
181-
2. After you log in to GitGuardian and authorize the application, you'll be redirected back to the local server
182-
3. The authentication token will be securely stored for future use
183-
4. The next time you start the server, it will reuse the stored token without requiring re-authentication
180+
The GitGuardian MCP server supports multiple authentication methods depending on your deployment mode.
181+
182+
### OAuth Authentication (Default for stdio transport)
183+
184+
When using stdio transport (the default for desktop IDE integrations), the server uses OAuth for authentication by default:
185+
186+
1. OAuth is **enabled by default** (`ENABLE_LOCAL_OAUTH=true`) for local-first usage
187+
2. When you start the server, it will automatically open a browser window to authenticate with GitGuardian
188+
3. After you log in to GitGuardian and authorize the application, you'll be redirected back to the local server
189+
4. The authentication token will be securely stored in `~/.gitguardian/` for future use
190+
5. The next time you start the server, it will reuse the stored token without requiring re-authentication
191+
192+
**Example configuration (OAuth is enabled by default, no need to specify):**
193+
194+
```json
195+
{
196+
"mcpServers": {
197+
"GitGuardianDeveloper": {
198+
"command": "uvx",
199+
"args": [
200+
"--from",
201+
"git+https://github.com/GitGuardian/ggmcp.git",
202+
"developer-mcp-server"
203+
]
204+
}
205+
}
206+
}
207+
```
208+
209+
**To disable OAuth** (e.g., for using PAT instead):
210+
211+
```json
212+
{
213+
"mcpServers": {
214+
"GitGuardianDeveloper": {
215+
"command": "uvx",
216+
"args": [
217+
"--from",
218+
"git+https://github.com/GitGuardian/ggmcp.git",
219+
"developer-mcp-server"
220+
],
221+
"env": {
222+
"ENABLE_LOCAL_OAUTH": "false",
223+
"GITGUARDIAN_PERSONAL_ACCESS_TOKEN": "your_pat_here"
224+
}
225+
}
226+
}
227+
}
228+
```
229+
230+
### Personal Access Token (PAT) Authentication
231+
232+
For non-interactive environments, CI/CD pipelines, or when you prefer not to use OAuth, you can authenticate using a Personal Access Token:
233+
234+
1. Create a Personal Access Token in your GitGuardian dashboard
235+
2. Set the `GITGUARDIAN_PERSONAL_ACCESS_TOKEN` environment variable
236+
237+
**Example configuration with PAT:**
238+
239+
```json
240+
{
241+
"mcpServers": {
242+
"GitGuardianDeveloper": {
243+
"command": "uvx",
244+
"args": [
245+
"--from",
246+
"git+https://github.com/GitGuardian/ggmcp.git",
247+
"developer-mcp-server"
248+
],
249+
"env": {
250+
"GITGUARDIAN_PERSONAL_ACCESS_TOKEN": "your_personal_access_token_here"
251+
}
252+
}
253+
}
254+
}
255+
```
256+
257+
### Per-Request Authentication (HTTP/SSE transport)
258+
259+
When using HTTP/SSE transport (with `MCP_PORT` set), the server expects authentication via the `Authorization` header in each HTTP request. This is the recommended approach for server deployments.
260+
261+
**Important:** Since `ENABLE_LOCAL_OAUTH` defaults to `true`, you **must explicitly set it to `false`** when using HTTP/SSE mode:
262+
263+
```bash
264+
# Start server with HTTP transport (OAuth must be disabled)
265+
ENABLE_LOCAL_OAUTH=false MCP_PORT=8000 MCP_HOST=127.0.0.1 uvx --from git+https://github.com/GitGuardian/ggmcp.git developer-mcp-server
266+
267+
# Make authenticated request
268+
curl -X POST http://127.0.0.1:8000/tools/list \
269+
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN" \
270+
-H "Content-Type: application/json" \
271+
-d '{}'
272+
```
273+
274+
**Configuration validation:** The server will raise an error if both `MCP_PORT` and `ENABLE_LOCAL_OAUTH=true` are set, as HTTP/SSE mode requires per-request authentication for security reasons.
184275

185276
## Configuration for Different GitGuardian Instances
186277

@@ -197,7 +288,9 @@ The following environment variables can be configured:
197288
| `GITGUARDIAN_SCOPES` | OAuth scopes to request | Auto-detected based on instance type | `scan,incidents:read,sources:read,honeytokens:read,honeytokens:write` |
198289
| `GITGUARDIAN_TOKEN_NAME` | Name for the OAuth token | Auto-generated based on server type | `"Developer MCP Token"` |
199290
| `GITGUARDIAN_TOKEN_LIFETIME` | Token lifetime in days | `30` | `60` or `never` |
200-
| `MCP_PORT` | Port for HTTP/SSE transport (when set, enables HTTP transport instead of stdio) | Not set (uses stdio) | `8000` |
291+
| `GITGUARDIAN_PERSONAL_ACCESS_TOKEN` | Personal Access Token for authentication (alternative to OAuth) | Not set | `YOUR_PAT_TOKEN` |
292+
| `ENABLE_LOCAL_OAUTH` | Enable local OAuth flow (stdio mode only, cannot be used with `MCP_PORT`) | `true` (enabled by default for local-first usage) | `false` |
293+
| `MCP_PORT` | Port for HTTP/SSE transport (when set, enables HTTP transport instead of stdio, requires `ENABLE_LOCAL_OAUTH=false`) | Not set (uses stdio) | `8000` |
201294
| `MCP_HOST` | Host address for HTTP/SSE transport | `127.0.0.1` | `0.0.0.0` |
202295

203296
### HTTP/SSE Transport
@@ -206,7 +299,7 @@ By default, the MCP server uses **stdio transport** for local IDE integrations.
206299

207300
#### Enabling HTTP Transport
208301

209-
To enable HTTP/SSE transport, set the `MCP_PORT` environment variable:
302+
To enable HTTP/SSE transport, set the `MCP_PORT` environment variable. **Important:** You must also set `ENABLE_LOCAL_OAUTH=false` since OAuth defaults to enabled:
210303

211304
```json
212305
{
@@ -219,6 +312,7 @@ To enable HTTP/SSE transport, set the `MCP_PORT` environment variable:
219312
"developer-mcp-server"
220313
],
221314
"env": {
315+
"ENABLE_LOCAL_OAUTH": "false",
222316
"MCP_PORT": "8000",
223317
"MCP_HOST": "127.0.0.1"
224318
}
@@ -232,15 +326,15 @@ To enable HTTP/SSE transport, set the `MCP_PORT` environment variable:
232326
You can also run the server directly with HTTP transport:
233327

234328
```bash
235-
# Run with HTTP transport
236-
MCP_PORT=8000 MCP_HOST=127.0.0.1 uvx --from git+https://github.com/GitGuardian/ggmcp.git developer-mcp-server
329+
# Run with HTTP transport (must disable OAuth)
330+
ENABLE_LOCAL_OAUTH=false MCP_PORT=8000 MCP_HOST=127.0.0.1 uvx --from git+https://github.com/GitGuardian/ggmcp.git developer-mcp-server
237331
```
238332

239333
The server will automatically start on `http://127.0.0.1:8000` and be accessible for remote integrations.
240334

241335
#### Authentication via Authorization Header
242336

243-
When using HTTP/SSE transport, you can authenticate using a Personal Access Token (PAT) via the `Authorization` header. This is useful for remote integrations where environment variables or OAuth flows are not practical.
337+
When using HTTP/SSE transport, authentication is done via the `Authorization` header on each request. See the [Per-Request Authentication](#per-request-authentication-httpsse-transport) section for detailed configuration.
244338

245339
**Supported header formats:**
246340
- `Authorization: Bearer <token>`
@@ -285,11 +379,10 @@ async with httpx.AsyncClient() as client:
285379
**Authentication Priority:**
286380

287381
When using HTTP transport, the authentication priority is:
288-
1. **Authorization header** (if present in the HTTP request)
289-
2. **GITGUARDIAN_PERSONAL_ACCESS_TOKEN** environment variable
290-
3. **OAuth flow** (default fallback)
382+
1. **Authorization header** (if present in the HTTP request) - recommended for HTTP/SSE mode
383+
2. **GITGUARDIAN_PERSONAL_ACCESS_TOKEN** environment variable - fallback option
291384

292-
This allows different clients to use different authentication methods when connecting to the same HTTP server instance.
385+
Note that OAuth (`ENABLE_LOCAL_OAUTH=true`) is not supported in HTTP/SSE mode for security reasons. Each HTTP request must include its own authentication credentials.
293386

294387
**Notes:**
295388
- `uvicorn` is included as a dependency - no additional installation needed.
@@ -388,17 +481,19 @@ This project includes a comprehensive test suite to ensure functionality and pre
388481

389482
2. Run the test suite:
390483
```bash
391-
uv run pytest
484+
ENABLE_LOCAL_OAUTH=false uv run pytest
392485
```
393486

487+
Note: Tests disable OAuth by default via the `ENABLE_LOCAL_OAUTH=false` environment variable to prevent OAuth prompts during test execution.
488+
394489
3. Run tests with verbose output:
395490
```bash
396-
uv run pytest -v
491+
ENABLE_LOCAL_OAUTH=false uv run pytest -v
397492
```
398493

399494
4. Run tests with coverage:
400495
```bash
401-
uv run pytest --cov=packages --cov-report=html
496+
ENABLE_LOCAL_OAUTH=false uv run pytest --cov=packages --cov-report=html
402497
```
403498

404499
This will run all tests and generate a coverage report showing which parts of the codebase are covered by tests.

packages/developer_mcp_server/src/developer_mcp_server/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
import os
55

6-
from gg_api_core.mcp_server import GitGuardianFastMCP
6+
from gg_api_core.mcp_server import get_mcp_server
77
from gg_api_core.scopes import set_developer_scopes
88

99
from developer_mcp_server.register_tools import DEVELOPER_INSTRUCTIONS, register_developer_tools
@@ -14,7 +14,7 @@
1414
logger = logging.getLogger(__name__)
1515

1616
# Use our custom GitGuardianFastMCP from the core package
17-
mcp = GitGuardianFastMCP(
17+
mcp = get_mcp_server(
1818
"GitGuardian Developer",
1919
log_level="DEBUG",
2020
instructions=DEVELOPER_INSTRUCTIONS,

packages/gg_api_core/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ from gg_api_core.mcp_server import GitGuardianFastMCP
2323
from gg_api_core.client import GitGuardianClient
2424

2525
# Create a custom MCP server
26-
mcp = GitGuardianFastMCP("My Custom Server")
26+
mcp = get_mcp_server("My Custom Server")
27+
2728

2829
# Register tools that use the GitGuardian API
2930
@mcp.tool(required_scopes=["honeytokens:read"])

packages/gg_api_core/src/gg_api_core/client.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@ class TagNames(str, Enum):
6767
REVOCABLE_BY_GG = "REVOCABLE_BY_GG"
6868

6969

70+
def is_oauth_enabled() -> bool:
71+
"""
72+
Check if OAuth authentication is enabled via environment variable.
73+
"""
74+
if os.environ.get("ENABLE_LOCAL_OAUTH") is None:
75+
# Default value is True
76+
return True
77+
return os.environ.get("ENABLE_LOCAL_OAUTH", "").lower() == "true"
78+
79+
80+
def get_personal_access_token_from_env() -> str | None:
81+
return os.environ.get("GITGUARDIAN_PERSONAL_ACCESS_TOKEN")
82+
83+
7084
class GitGuardianClient:
7185
"""Client for interacting with the GitGuardian API."""
7286

@@ -105,16 +119,33 @@ def _init_urls(self, gitguardian_url: str | None = None):
105119
logger.info(f"Using private API URL: {self.private_api_url}")
106120

107121
def _init_personal_access_token(self, personal_access_token: str | None = None):
122+
# Validate OAuth configuration
123+
mcp_port = os.environ.get("MCP_PORT")
124+
enable_local_oauth = is_oauth_enabled()
125+
108126
if personal_access_token:
109127
logger.info("Using provided PAT")
110128
self._oauth_token = personal_access_token
111-
elif personal_access_token := os.environ.get("GITGUARDIAN_PERSONAL_ACCESS_TOKEN"):
112-
logger.info("Using PAT from environment variable")
113-
self._oauth_token = personal_access_token
129+
return
130+
131+
if mcp_port:
132+
if enable_local_oauth:
133+
raise ValueError(
134+
"Invalid configuration: Cannot use ENABLE_LOCAL_OAUTH=true with MCP_PORT set. "
135+
"HTTP/SSE mode requires per-request authentication via Authorization headers. "
136+
"For local OAuth authentication, use stdio transport (unset MCP_PORT)."
137+
)
114138
else:
115-
# TODO(APPAI): We should also locate here the retrieval from storage
116-
logger.info("No PAT provided, falling back to OAuth")
117-
self._oauth_token = None
139+
if personal_access_token:
140+
logger.info("Using provided PAT")
141+
self._oauth_token = personal_access_token
142+
elif personal_access_token := os.environ.get("GITGUARDIAN_PERSONAL_ACCESS_TOKEN"):
143+
logger.info("Using PAT from environment variable")
144+
self._oauth_token = personal_access_token
145+
else:
146+
# TODO(APPAI): We should also locate here the retrieval from storage
147+
logger.info("No PAT provided, falling back to OAuth")
148+
self._oauth_token = None
118149

119150
def _normalize_api_url(self, api_url: str) -> str:
120151
"""
@@ -221,12 +252,20 @@ def _get_dashboard_url(self) -> str:
221252
logger.warning(f"Failed to extract dashboard URL from API URL: {e}")
222253
return default_dashboard_url
223254

224-
async def _ensure_oauth_token(self):
225-
"""Ensure we have a valid OAuth token, initiating the OAuth flow if needed."""
255+
async def _ensure_api_token(self):
256+
"""Ensure we have a valid token, initiating the OAuth flow if needed.
257+
258+
OAuth flow is only enabled when ENABLE_LOCAL_OAUTH=true.
259+
This prevents OAuth prompts in HTTP/SSE mode (which uses per-request PATs)
260+
and in test environments.
261+
"""
226262

227263
if self._oauth_token is not None:
228264
return
229265

266+
if not is_oauth_enabled():
267+
raise RuntimeError("OAuth is not enabled")
268+
230269
# Use a global lock to prevent parallel OAuth flows across all client instances
231270
async with _oauth_lock:
232271
# Double-check pattern: another thread might have completed OAuth while we waited for the lock
@@ -322,7 +361,7 @@ async def _clear_invalid_oauth_token(self):
322361
logger.warning(f"Could not clean up token storage: {str(e)}")
323362

324363
# Force new OAuth flow on next request
325-
await self._ensure_oauth_token()
364+
await self._ensure_api_token()
326365

327366
async def _request(
328367
self, method: str, endpoint: str, return_headers: bool = False, **kwargs
@@ -359,7 +398,7 @@ async def _request(
359398
logger.debug(f"Request body: {safe_json}")
360399

361400
# Ensure we have a valid OAuth token
362-
await self._ensure_oauth_token()
401+
await self._ensure_api_token()
363402
headers = {
364403
"Authorization": f"Token {self._oauth_token}",
365404
"Content-Type": "application/json",

0 commit comments

Comments
 (0)