Skip to content

Commit f7f8381

Browse files
Fix issue #108 (#111)
* Fix auth system issues Issue 1: Config file endpoint not applied - Transfer all config values to args namespace before validation - CLI arguments take precedence over config file - 38 settings now transferred (endpoint, protocol, mode, timeout, etc) Issue 3: HTTP auth headers never applied - Integrate create_transport_with_auth() into client/__init__.py - Auth headers now applied to all HTTP/HTTPS requests - Enhanced logging at DEBUG level Issue 4: OAuth provider design - OAuth correctly uses header-based auth (RFC 6750) - Added documentation explaining why params are empty Issue 5: API Key auth ignores custom prefix - Add customizable prefix parameter to APIKeyAuth - Supports Bearer, Token, Custom, or empty prefix - Updated factory functions and config loaders Issue 6: Poor error messages for auth config - Add field-level validation for each auth type - Show which field is missing with example config - Include provider name in error messages * Fix ruff linting errors: line length and f-string formatting * Update documentation: code quality guidelines and authentication system reference * fix ci * resolve comments
1 parent bcb4bdf commit f7f8381

File tree

15 files changed

+974
-132
lines changed

15 files changed

+974
-132
lines changed

docs/configuration/configuration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ auth:
117117
username: "${USERNAME}"
118118
password: "${PASSWORD}"
119119

120-
tool_mappings:
120+
tool_mapping:
121121
openai_chat: openai_api
122122
github_search: github_api
123123
secure_tool: basic_auth
@@ -229,7 +229,7 @@ type = "basic"
229229
username = "${USERNAME}"
230230
password = "${PASSWORD}"
231231
232-
[auth.tool_mappings]
232+
[auth.tool_mapping]
233233
openai_chat = "openai_api"
234234
github_search = "github_api"
235235
secure_tool = "basic_auth"

docs/development/contributing.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,14 +204,27 @@ repos:
204204
205205
- **Python**: Follow PEP 8 style guide
206206
207-
- **Line Length**: Maximum 88 characters (Black default)
207+
- **Line Length**: Maximum 88 characters (enforced by Ruff)
208+
- Break long lines across multiple statements
209+
- Use implicit line continuation for function signatures and strings
210+
- Avoid f-strings without placeholders
211+
212+
- **F-strings**: Always include placeholders when using f-strings
213+
- Use regular strings for static messages
214+
- Extract complex expressions to variables if needed
208215
209216
- **Imports**: Use absolute imports, group by standard library, third-party, local
210217
211218
- **Type Hints**: Use type hints for all function parameters and return values
212219
213220
- **Documentation**: Include docstrings for all public functions and classes
214221
222+
- **Logging**: Use appropriate logging levels
223+
- DEBUG: Tool call notifications and detailed execution flow
224+
- INFO: Important state changes and user-facing information
225+
- WARNING: Potential issues that don't prevent execution
226+
- ERROR: Errors that require attention
227+
215228
## Documentation
216229
217230
### Building Documentation

docs/development/reference.md

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,95 @@ Notes:
9595
|----------|-------------|
9696
| `MCP_API_KEY` | API key for authentication |
9797
| `MCP_HEADER_NAME` | Header name for API key (default: Authorization) |
98+
| `MCP_PREFIX` | Prefix for API key value (default: Bearer) |
9899
| `MCP_USERNAME` | Username for basic authentication |
99100
| `MCP_PASSWORD` | Password for basic authentication |
100101
| `MCP_OAUTH_TOKEN` | OAuth token for authentication |
102+
| `MCP_CUSTOM_HEADERS` | Custom headers as JSON string for authentication |
103+
| `MCP_TOOL_AUTH_MAPPING` | Map tool names to auth providers as JSON |
104+
105+
## Authentication System Reference
106+
107+
### Authentication Providers
108+
109+
The authentication system supports multiple provider types with configurable options:
110+
111+
#### API Key Authentication
112+
113+
```json
114+
{
115+
"type": "api_key",
116+
"api_key": "YOUR_API_KEY",
117+
"header_name": "Authorization",
118+
"prefix": "Bearer"
119+
}
120+
```
121+
122+
- **api_key** (required): The API key value
123+
- **header_name** (optional): HTTP header to place the key in (default: "Authorization")
124+
- **prefix** (optional): Value prefix for the header (default: "Bearer"). Set to empty string for no prefix.
125+
126+
#### Basic Authentication
127+
128+
```json
129+
{
130+
"type": "basic",
131+
"username": "user",
132+
"password": "password"
133+
}
134+
```
135+
136+
- **username** (required): Username for basic auth
137+
- **password** (required): Password for basic auth
138+
139+
#### OAuth Token Authentication
140+
141+
```json
142+
{
143+
"type": "oauth",
144+
"token": "YOUR_TOKEN",
145+
"token_type": "Bearer"
146+
}
147+
```
148+
149+
- **token** (required): OAuth token value
150+
- **token_type** (optional): Token type for Authorization header (default: "Bearer")
151+
152+
#### Custom Headers Authentication
153+
154+
```json
155+
{
156+
"type": "custom",
157+
"headers": {
158+
"X-Custom-Header": "value",
159+
"X-Another-Header": "another-value"
160+
}
161+
}
162+
```
163+
164+
- **headers** (required): Dictionary of custom headers to include
165+
166+
### Tool-to-Auth Mapping
167+
168+
Map specific tools to authentication providers:
169+
170+
```json
171+
{
172+
"tool_mapping": {
173+
"openai_chat": "openai_api",
174+
"github_search": "github_api",
175+
"default_tool": "basic_auth"
176+
}
177+
}
178+
```
179+
180+
### Error Messages
181+
182+
The authentication system provides detailed error messages for configuration issues:
183+
184+
- Missing required fields indicate which provider type and field is missing
185+
- Expected configuration format is provided in error messages
186+
- Type validation errors show the received vs. expected type
101187

102188
## Runtime Management API Reference
103189

@@ -1210,7 +1296,7 @@ mcp-fuzzer --mode tools --protocol http --endpoint http://localhost:8000 --tool-
12101296
"password": "password"
12111297
}
12121298
},
1213-
"tool_mappings": {
1299+
"tool_mapping": {
12141300
"openai_chat": "openai_api",
12151301
"github_search": "github_api",
12161302
"secure_tool": "basic_auth"

docs/getting-started/examples.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ Create `auth_config.json`:
107107
"header_name": "Authorization"
108108
}
109109
},
110-
"tool_mappings": {
110+
"tool_mapping": {
111111
"openai_chat": "openai_api",
112112
"github_search": "github_api"
113113
}

docs/getting-started/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ Create `auth_config.json`:
196196
"password": "password"
197197
}
198198
},
199-
"tool_mappings": {
199+
"tool_mapping": {
200200
"openai_chat": "openai_api",
201201
"github_search": "github_api",
202202
"secure_tool": "basic_auth"

examples/auth_config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
2+
"default_provider": "api_key",
23
"providers": {
34
"api_key": { "type": "api_key", "api_key": "secret123", "header_name": "Authorization" }
45
},
5-
"tool_mappings": {
6+
"tool_mapping": {
67
"secure_tool": "api_key"
78
}
89
}

mcp_fuzzer/auth/loaders.py

Lines changed: 123 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import logging
23
import os
34

45
from .manager import AuthManager
@@ -9,12 +10,23 @@
910
create_custom_header_auth,
1011
)
1112

13+
logger = logging.getLogger(__name__)
14+
1215
def setup_auth_from_env() -> AuthManager:
1316
auth_manager = AuthManager()
1417

1518
api_key = os.getenv("MCP_API_KEY")
19+
header_name = os.getenv("MCP_HEADER_NAME")
20+
prefix = os.getenv("MCP_PREFIX")
1621
if api_key:
17-
auth_manager.add_auth_provider("api_key", create_api_key_auth(api_key))
22+
auth_manager.add_auth_provider(
23+
"api_key",
24+
create_api_key_auth(
25+
api_key,
26+
header_name if header_name is not None else "Authorization",
27+
prefix if prefix is not None else "Bearer",
28+
),
29+
)
1830

1931
username = os.getenv("MCP_USERNAME")
2032
password = os.getenv("MCP_PASSWORD")
@@ -36,8 +48,8 @@ def setup_auth_from_env() -> AuthManager:
3648
auth_manager.add_auth_provider(
3749
"custom", create_custom_header_auth(headers)
3850
)
39-
except (json.JSONDecodeError, TypeError):
40-
pass
51+
except (json.JSONDecodeError, TypeError) as exc:
52+
logger.debug("Failed to parse MCP_CUSTOM_HEADERS as JSON: %s", exc)
4153

4254
tool_mapping = os.getenv("MCP_TOOL_AUTH_MAPPING")
4355
if tool_mapping:
@@ -48,8 +60,12 @@ def setup_auth_from_env() -> AuthManager:
4860
auth_manager.map_tool_to_auth(
4961
str(tool_name), str(auth_provider_name)
5062
)
51-
except (json.JSONDecodeError, TypeError):
52-
pass
63+
except (json.JSONDecodeError, TypeError) as exc:
64+
logger.debug("Failed to parse MCP_TOOL_AUTH_MAPPING as JSON: %s", exc)
65+
66+
default_provider = os.getenv("MCP_DEFAULT_AUTH_PROVIDER")
67+
if default_provider:
68+
auth_manager.set_default_provider(default_provider)
5369

5470
return auth_manager
5571

@@ -64,41 +80,109 @@ def load_auth_config(config_file: str) -> AuthManager:
6480

6581
providers = config.get("providers", {})
6682
for name, provider_config in providers.items():
67-
provider_type = provider_config.get("type")
68-
if provider_type == "api_key":
69-
auth_manager.add_auth_provider(
70-
name,
71-
create_api_key_auth(
72-
provider_config["api_key"],
73-
provider_config.get("header_name", "Authorization"),
74-
),
75-
)
76-
elif provider_type == "basic":
77-
auth_manager.add_auth_provider(
78-
name,
79-
create_basic_auth(
80-
provider_config["username"], provider_config["password"]
81-
),
83+
if not isinstance(provider_config, dict):
84+
raise ValueError(
85+
f"Error configuring auth provider '{name}': "
86+
f"expected an object, got {type(provider_config).__name__}"
8287
)
83-
elif provider_type == "oauth":
84-
auth_manager.add_auth_provider(
85-
name,
86-
create_oauth_auth(
87-
provider_config["token"],
88-
provider_config.get("token_type", "Bearer"),
89-
),
90-
)
91-
elif provider_type == "custom":
92-
headers = provider_config.get("headers")
93-
if not isinstance(headers, dict):
94-
raise ValueError(f"Provider '{name}' custom headers must be a dict")
95-
headers_str: dict[str, str] = {str(k): str(v) for k, v in headers.items()}
96-
auth_manager.add_auth_provider(name, create_custom_header_auth(headers_str))
97-
else:
98-
raise ValueError(f"Unknown provider type: {provider_type}")
99-
100-
tool_mappings = config.get("tool_mapping", {})
101-
for tool_name, auth_provider_name in tool_mappings.items():
88+
provider_type = provider_config.get("type")
89+
90+
try:
91+
if provider_type == "api_key":
92+
if "api_key" not in provider_config:
93+
raise ValueError(
94+
f"Provider '{name}' is type 'api_key' but missing "
95+
"required field 'api_key'. Expected: "
96+
"{'type': 'api_key', 'api_key': 'YOUR_API_KEY'}"
97+
)
98+
auth_manager.add_auth_provider(
99+
name,
100+
create_api_key_auth(
101+
provider_config["api_key"],
102+
provider_config.get("header_name", "Authorization"),
103+
provider_config.get("prefix", "Bearer"),
104+
),
105+
)
106+
elif provider_type == "basic":
107+
if "username" not in provider_config:
108+
raise ValueError(
109+
f"Provider '{name}' is type 'basic' but missing "
110+
"required field 'username'. Expected: "
111+
"{'type': 'basic', 'username': 'user', 'password': 'pass'}"
112+
)
113+
if "password" not in provider_config:
114+
raise ValueError(
115+
f"Provider '{name}' is type 'basic' but missing "
116+
"required field 'password'. Expected: "
117+
"{'type': 'basic', 'username': 'user', 'password': 'pass'}"
118+
)
119+
auth_manager.add_auth_provider(
120+
name,
121+
create_basic_auth(
122+
provider_config["username"], provider_config["password"]
123+
),
124+
)
125+
elif provider_type == "oauth":
126+
if "token" not in provider_config:
127+
raise ValueError(
128+
f"Provider '{name}' is type 'oauth' but missing "
129+
"required field 'token'. Expected: "
130+
"{'type': 'oauth', 'token': 'YOUR_TOKEN'}"
131+
)
132+
auth_manager.add_auth_provider(
133+
name,
134+
create_oauth_auth(
135+
provider_config["token"],
136+
provider_config.get("token_type", "Bearer"),
137+
),
138+
)
139+
elif provider_type == "custom":
140+
headers = provider_config.get("headers")
141+
if not headers:
142+
raise ValueError(
143+
f"Provider '{name}' is type 'custom' but missing "
144+
"required field 'headers'. Expected: "
145+
"{'type': 'custom', 'headers': {'X-Header': 'value'}}"
146+
)
147+
if not isinstance(headers, dict):
148+
raise ValueError(
149+
f"Provider '{name}' custom headers must be a dict, "
150+
f"got {type(headers).__name__}"
151+
)
152+
headers_str: dict[str, str] = {
153+
str(k): str(v) for k, v in headers.items()
154+
}
155+
auth_manager.add_auth_provider(
156+
name, create_custom_header_auth(headers_str)
157+
)
158+
else:
159+
raise ValueError(
160+
f"Unknown provider type: '{provider_type}' for provider '{name}'. "
161+
f"Supported types: api_key, basic, oauth, custom"
162+
)
163+
except (KeyError, ValueError) as e:
164+
raise ValueError(f"Error configuring auth provider '{name}': {str(e)}")
165+
166+
tool_mappings = config.get("tool_mapping")
167+
legacy_tool_mappings = config.get("tool_mappings")
168+
if tool_mappings and legacy_tool_mappings:
169+
raise ValueError(
170+
"Both 'tool_mapping' and legacy 'tool_mappings' are defined. "
171+
"Please use only 'tool_mapping'."
172+
)
173+
174+
final_tool_mappings = tool_mappings or legacy_tool_mappings or {}
175+
if final_tool_mappings and not isinstance(final_tool_mappings, dict):
176+
raise ValueError(
177+
f"'tool_mapping' must be a dict, "
178+
f"got {type(final_tool_mappings).__name__}"
179+
)
180+
181+
for tool_name, auth_provider_name in final_tool_mappings.items():
102182
auth_manager.map_tool_to_auth(tool_name, auth_provider_name)
103183

184+
default_provider = config.get("default_provider")
185+
if default_provider:
186+
auth_manager.set_default_provider(default_provider)
187+
104188
return auth_manager

0 commit comments

Comments
 (0)