Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions py2mcp/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ def mk_auth_provider(auth: Optional[dict]) -> Optional[Any]:

- ``jwks_uri`` *or* ``public_key`` — where to get the IdP's signing key(s).
- ``issuer`` — the IdP issuer URL (the token's ``iss``).
- ``audience`` — **this** server's resource id (the token's ``aud``; RFC 8707
audience binding that stops a token for another service being replayed here).
- ``audience`` (**required**) — **this** server's resource id (the token's
``aud``). RFC 8707 audience binding is mandatory: it stops a token minted for
another service being replayed here (the confused-deputy defense), so this
helper refuses to build a verifier that would skip it.
- ``authorization_servers`` (or a single ``issuer``) — IdP issuer URL(s)
advertised in the RFC 9728 protected-resource metadata.
- ``base_url`` — this server's public base URL.
Expand Down Expand Up @@ -96,6 +98,15 @@ def mk_auth_provider(auth: Optional[dict]) -> Optional[Any]:
"jwt auth needs 'base_url' (this server's public base URL, for the "
"RFC 9728 protected-resource metadata)."
)
audience = auth.get("audience")
if not audience:
raise ValueError(
"jwt auth needs 'audience' (this server's resource id). Without it the "
"token's audience is NOT validated, which re-opens the confused-deputy "
"vulnerability (a token minted for another service could be replayed "
"here) — RFC 8707 audience binding is mandatory for an MCP resource "
"server. Set it to this connector's public resource URL."
)
authorization_servers = auth.get("authorization_servers")
if not authorization_servers and auth.get("issuer"):
authorization_servers = [auth["issuer"]]
Expand All @@ -110,7 +121,7 @@ def mk_auth_provider(auth: Optional[dict]) -> Optional[Any]:
jwks_uri=jwks_uri,
public_key=public_key,
issuer=auth.get("issuer"),
audience=auth.get("audience"),
audience=audience,
required_scopes=required_scopes,
base_url=base_url,
)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "hatchling.build"

[project]
name = "py2mcp"
version = "0.1.4"
version = "0.1.5"
description = "Quick MCP server creation from Python functions"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
8 changes: 8 additions & 0 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ def test_missing_base_url_raises():
mk_auth_provider(auth)


def test_missing_audience_raises():
# RFC 8707 audience binding is mandatory — without it JWTVerifier would skip
# audience validation (confused-deputy risk), so the helper must refuse.
auth = {k: v for k, v in _AUTH.items() if k != "audience"}
with pytest.raises(ValueError, match="audience"):
mk_auth_provider(auth)


def test_missing_authorization_servers_raises():
auth = {
k: v
Expand Down
Loading