This is an automated email from the ASF dual-hosted git repository.
FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git
The following commit(s) were added to refs/heads/master by this push:
new 6500b47 fix: bind authorization code exchanges to resource (#121)
6500b47 is described below
commit 6500b471041a6ad8a3051d6f0a532ebab238769b
Author: Yijia Su <[email protected]>
AuthorDate: Wed Jul 29 22:01:53 2026 +0800
fix: bind authorization code exchanges to resource (#121)
---
CHANGELOG.md | 2 +
README.md | 5 ++-
doris_mcp_server/auth/doris_oauth_provider.py | 6 +++
test/auth/test_doris_oauth_routes.py | 57 +++++++++++++++++++++++++++
4 files changed, 69 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c95c49..448d9a5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -65,6 +65,8 @@ under **Unreleased** until a new version is selected and
published.
metadata, and operation-specific insufficient-scope responses.
- Bound Doris OAuth access tokens to the canonical MCP resource and rejected
tokens issued for any other resource before per-user pool access.
+- Rejected Doris OAuth authorization-code exchanges whose required RFC 8707
+ `resource` is missing or differs from the authorization grant.
- Required Doris OAuth DCR clients to declare a persisted `application_type`
and enforced type-matched redirect URI rules for native and web clients.
- Added RFC 9207 `iss` identification to Doris OAuth authorization success and
diff --git a/README.md b/README.md
index 12cb5f6..d0e634b 100644
--- a/README.md
+++ b/README.md
@@ -683,7 +683,10 @@ Each Doris OAuth token record is bound to the resource
selected during
authorization. The protected MCP endpoint accepts only the exact canonical
resource `${DORIS_OAUTH_BASE_URL}/mcp`; a token issued for the authorization
server or any other resource is rejected with an `invalid_token` challenge
-before a per-user Doris pool is used.
+before a per-user Doris pool is used. Clients must send that canonical
+`resource` in both the authorization request and authorization-code token
+request. The token endpoint returns RFC 8707 `invalid_target` if the value is
+missing or does not exactly match the resource bound to the authorization code.
#### Minimal Local Configuration
diff --git a/doris_mcp_server/auth/doris_oauth_provider.py
b/doris_mcp_server/auth/doris_oauth_provider.py
index b328e54..28bc2c2 100644
--- a/doris_mcp_server/auth/doris_oauth_provider.py
+++ b/doris_mcp_server/auth/doris_oauth_provider.py
@@ -291,6 +291,12 @@ class DorisOAuthProvider:
raise TokenEndpointError("invalid_grant", "Authorization code is
invalid or expired", status_code=400)
if not self._verify_pkce(record.code_challenge,
payload.get("code_verifier") or ""):
raise TokenEndpointError("invalid_grant", "Authorization code is
invalid or expired", status_code=400)
+ if payload.get("resource") != record.resource:
+ raise TokenEndpointError(
+ "invalid_target",
+ "resource is required and must match the authorization grant",
+ status_code=400,
+ )
if payload.get("scope"):
requested =
set(self.scope_policy.parse_scope(payload.get("scope")))
if not requested <= set(record.scopes):
diff --git a/test/auth/test_doris_oauth_routes.py
b/test/auth/test_doris_oauth_routes.py
index dee8c83..625dcc7 100644
--- a/test/auth/test_doris_oauth_routes.py
+++ b/test/auth/test_doris_oauth_routes.py
@@ -660,6 +660,7 @@ async def
test_authorize_invalid_redirect_is_direct_400_and_invalid_scope_redire
"state": "state-1",
"code_challenge": challenge,
"code_challenge_method": "S256",
+ "resource": provider.resource,
},
)
assert response.status_code == 400
@@ -807,6 +808,7 @@ async def
test_full_login_code_exchange_auth_context_and_pool_missing_revocation
"code": code,
"redirect_uri": "http://localhost:7777/callback",
"code_verifier": verifier,
+ "resource": provider.resource,
},
)
assert token_response.status_code == 200
@@ -869,6 +871,59 @@ async def
test_access_token_for_other_resource_is_rejected_before_pool_access():
assert provider.store.get_access_token(pair.access_token).last_used_at is
None
[email protected](
+ "token_resource",
+ [
+ pytest.param(None, id="missing"),
+ pytest.param("http://localhost:3000", id="different-resource"),
+ ],
+)
[email protected]
+async def test_authorization_code_exchange_rejects_unbound_resource(
+ token_resource,
+):
+ provider, _cm, app = _provider_app()
+ client_record = provider.store.add_client(
+ client_id="resource-exchange-client",
+ client_secret=None,
+ token_endpoint_auth_method="none",
+ application_type="native",
+ redirect_uris=("http://localhost:7777/callback",),
+ client_allowed_scopes=("tool:list",),
+ source="dcr",
+ expires_at=None,
+ )
+ challenge, verifier = _pkce()
+ code, _record = provider.store.create_authorization_code(
+ client_id=client_record.client_id,
+ doris_user="alice",
+ redirect_uri="http://localhost:7777/callback",
+ scopes=("tool:list",),
+ resource=provider.resource,
+ code_challenge=challenge,
+ code_challenge_method="S256",
+ ttl_seconds=300,
+ )
+ payload = {
+ "grant_type": "authorization_code",
+ "client_id": client_record.client_id,
+ "code": code,
+ "redirect_uri": "http://localhost:7777/callback",
+ "code_verifier": verifier,
+ }
+ if token_resource is not None:
+ payload["resource"] = token_resource
+
+ async with await _client(app) as client:
+ response = await client.post("/oauth/token", data=payload)
+
+ assert response.status_code == 400
+ assert response.json()["error"] == "invalid_target"
+ assert provider.store.access_by_hash == {}
+ assert provider.store.refresh_by_hash == {}
+ assert provider.store.pop_authorization_code(code) is None
+
+
@pytest.mark.asyncio
async def
test_full_login_without_scope_grants_configured_rbac_capability_envelope():
provider, cm, app = _provider_app(
@@ -894,6 +949,7 @@ async def
test_full_login_without_scope_grants_configured_rbac_capability_envelo
"state": "state-full-default",
"code_challenge": challenge,
"code_challenge_method": "S256",
+ "resource": provider.resource,
},
)
assert authorize.status_code == 302
@@ -921,6 +977,7 @@ async def
test_full_login_without_scope_grants_configured_rbac_capability_envelo
"code": code,
"redirect_uri": "http://localhost:7777/callback",
"code_verifier": verifier,
+ "resource": provider.resource,
},
)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]