Skip to content

Commit 8605d25

Browse files
authored
Merge branch 'main' into fix/elicitation-str-enum
2 parents d04ba8c + a4bf947 commit 8605d25

File tree

3 files changed

+70
-10
lines changed

3 files changed

+70
-10
lines changed

CONTRIBUTING.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@ uv tool install pre-commit --with pre-commit-uv --force-reinstall
2323
## Development Workflow
2424

2525
1. Choose the correct branch for your changes:
26-
- For bug fixes to a released version: use the latest release branch (e.g. v1.1.x for 1.1.3)
27-
- For new features: use the main branch (which will become the next minor/major version)
28-
- If unsure, ask in an issue first
26+
27+
| Change Type | Target Branch | Example |
28+
|-------------|---------------|---------|
29+
| New features, breaking changes | `main` | New APIs, refactors |
30+
| Security fixes for v1 | `v1.x` | Critical patches |
31+
| Bug fixes for v1 | `v1.x` | Non-breaking fixes |
32+
33+
> **Note:** `main` is the v2 development branch. Breaking changes are welcome on `main`. The `v1.x` branch receives only security and critical bug fixes.
2934
3035
2. Create a new branch from your chosen base branch
3136

src/mcp/server/auth/handlers/token.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ async def handle(self, request: Request):
9797
# Authentication failures should return 401
9898
return PydanticJSONResponse(
9999
content=TokenErrorResponse(
100-
error="unauthorized_client",
100+
error="invalid_client",
101101
error_description=e.message,
102102
),
103103
status_code=401,

tests/server/fastmcp/auth/test_auth_integration.py

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -339,9 +339,59 @@ async def test_token_validation_error(self, test_client: httpx.AsyncClient):
339339
},
340340
)
341341
error_response = response.json()
342-
assert error_response["error"] == "unauthorized_client"
342+
# Per RFC 6749 Section 5.2, authentication failures (missing client_id)
343+
# must return "invalid_client", not "unauthorized_client"
344+
assert error_response["error"] == "invalid_client"
343345
assert "error_description" in error_response # Contains error message
344346

347+
@pytest.mark.anyio
348+
async def test_token_invalid_client_secret_returns_invalid_client(
349+
self,
350+
test_client: httpx.AsyncClient,
351+
registered_client: dict[str, Any],
352+
pkce_challenge: dict[str, str],
353+
mock_oauth_provider: MockOAuthProvider,
354+
):
355+
"""Test token endpoint returns 'invalid_client' for wrong client_secret per RFC 6749.
356+
357+
RFC 6749 Section 5.2 defines:
358+
- invalid_client: Client authentication failed (wrong credentials, unknown client)
359+
- unauthorized_client: Authenticated client not authorized for grant type
360+
361+
When client_secret is wrong, this is an authentication failure, so the
362+
error code MUST be 'invalid_client'.
363+
"""
364+
# Create an auth code for the registered client
365+
auth_code = f"code_{int(time.time())}"
366+
mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(
367+
code=auth_code,
368+
client_id=registered_client["client_id"],
369+
code_challenge=pkce_challenge["code_challenge"],
370+
redirect_uri=AnyUrl("https://client.example.com/callback"),
371+
redirect_uri_provided_explicitly=True,
372+
scopes=["read", "write"],
373+
expires_at=time.time() + 600,
374+
)
375+
376+
# Try to exchange the auth code with a WRONG client_secret
377+
response = await test_client.post(
378+
"/token",
379+
data={
380+
"grant_type": "authorization_code",
381+
"client_id": registered_client["client_id"],
382+
"client_secret": "wrong_secret_that_does_not_match",
383+
"code": auth_code,
384+
"code_verifier": pkce_challenge["code_verifier"],
385+
"redirect_uri": "https://client.example.com/callback",
386+
},
387+
)
388+
389+
assert response.status_code == 401
390+
error_response = response.json()
391+
# RFC 6749 Section 5.2: authentication failures MUST return "invalid_client"
392+
assert error_response["error"] == "invalid_client"
393+
assert "Invalid client_secret" in error_response["error_description"]
394+
345395
@pytest.mark.anyio
346396
async def test_token_invalid_auth_code(
347397
self,
@@ -1070,7 +1120,8 @@ async def test_wrong_auth_method_without_valid_credentials_fails(
10701120
)
10711121
assert response.status_code == 401
10721122
error_response = response.json()
1073-
assert error_response["error"] == "unauthorized_client"
1123+
# RFC 6749: authentication failures return "invalid_client"
1124+
assert error_response["error"] == "invalid_client"
10741125
assert "Client secret is required" in error_response["error_description"]
10751126

10761127
@pytest.mark.anyio
@@ -1114,7 +1165,8 @@ async def test_basic_auth_without_header_fails(
11141165
)
11151166
assert response.status_code == 401
11161167
error_response = response.json()
1117-
assert error_response["error"] == "unauthorized_client"
1168+
# RFC 6749: authentication failures return "invalid_client"
1169+
assert error_response["error"] == "invalid_client"
11181170
assert "Missing or invalid Basic authentication" in error_response["error_description"]
11191171

11201172
@pytest.mark.anyio
@@ -1158,7 +1210,8 @@ async def test_basic_auth_invalid_base64_fails(
11581210
)
11591211
assert response.status_code == 401
11601212
error_response = response.json()
1161-
assert error_response["error"] == "unauthorized_client"
1213+
# RFC 6749: authentication failures return "invalid_client"
1214+
assert error_response["error"] == "invalid_client"
11621215
assert "Invalid Basic authentication header" in error_response["error_description"]
11631216

11641217
@pytest.mark.anyio
@@ -1205,7 +1258,8 @@ async def test_basic_auth_no_colon_fails(
12051258
)
12061259
assert response.status_code == 401
12071260
error_response = response.json()
1208-
assert error_response["error"] == "unauthorized_client"
1261+
# RFC 6749: authentication failures return "invalid_client"
1262+
assert error_response["error"] == "invalid_client"
12091263
assert "Invalid Basic authentication header" in error_response["error_description"]
12101264

12111265
@pytest.mark.anyio
@@ -1252,7 +1306,8 @@ async def test_basic_auth_client_id_mismatch_fails(
12521306
)
12531307
assert response.status_code == 401
12541308
error_response = response.json()
1255-
assert error_response["error"] == "unauthorized_client"
1309+
# RFC 6749: authentication failures return "invalid_client"
1310+
assert error_response["error"] == "invalid_client"
12561311
assert "Client ID mismatch" in error_response["error_description"]
12571312

12581313
@pytest.mark.anyio

0 commit comments

Comments
 (0)