fix: forward resource_base_url and issuer_url through KeycloakOAuthProxy

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
strawgate 2026-05-18 22:05:29 -05:00
commit 2e1178fa84
2 changed files with 46 additions and 0 deletions

View file

@ -124,6 +124,8 @@ class KeycloakOAuthProxy(OAuthProxy):
upstream_client_id: str,
upstream_client_secret: str | None = None,
base_url: AnyHttpUrl | str,
resource_base_url: AnyHttpUrl | str | None = None,
issuer_url: AnyHttpUrl | str | None = None,
required_scopes: list[str] | str | None = None,
valid_scopes: list[str] | str | None = None,
audience: str | list[str] | None = None,
@ -149,6 +151,13 @@ class KeycloakOAuthProxy(OAuthProxy):
upstream_client_id: Client ID of the application registered in Keycloak.
upstream_client_secret: Client secret. Optional for public clients.
base_url: Public URL of this FastMCP server.
resource_base_url: Optional public base URL for the protected resource
metadata and token audience. Defaults to `base_url`. Set this when
the resource is served under a different origin/path than the
proxy (e.g. behind a gateway).
issuer_url: Issuer URL for OAuth metadata. Defaults to `base_url`. Set
this when the authorization-server metadata must advertise a
different issuer than `base_url` (e.g. behind a gateway).
required_scopes: Scopes to require on incoming tokens. Defaults to `["openid"]`.
valid_scopes: Scopes advertised to clients via the `/.well-known` endpoints
and accepted during Dynamic Client Registration. Defaults to
@ -224,6 +233,8 @@ class KeycloakOAuthProxy(OAuthProxy):
valid_scopes=parsed_valid_scopes,
redirect_path=redirect_path,
base_url=base_url,
resource_base_url=resource_base_url,
issuer_url=issuer_url,
jwt_signing_key=jwt_signing_key,
client_storage=client_storage,
require_authorization_consent=require_authorization_consent,

View file

@ -995,6 +995,41 @@ class TestKeycloakOAuthProxy:
)
assert proxy._redirect_path == "/custom/oauth/callback"
def test_issuer_and_resource_base_url_default_to_base_url(self, jwt_verifier):
proxy = KeycloakOAuthProxy(
realm_url="https://keycloak.example.com/realms/test",
upstream_client_id="test-client",
upstream_client_secret="test-secret",
token_verifier=jwt_verifier,
base_url="https://proxy.example.com",
jwt_signing_key="test-secret-key",
client_storage=MemoryStore(),
)
assert str(proxy.issuer_url).rstrip("/") == "https://proxy.example.com"
assert proxy.resource_base_url is None
def test_issuer_and_resource_base_url_forwarded(self, jwt_verifier):
"""Behind a gateway, switching to KeycloakOAuthProxy must not drop the
issuer/resource overrides that OAuthProxy supports (correct metadata
and JWT audience)."""
proxy = KeycloakOAuthProxy(
realm_url="https://keycloak.example.com/realms/test",
upstream_client_id="test-client",
upstream_client_secret="test-secret",
token_verifier=jwt_verifier,
base_url="https://internal.proxy.local",
issuer_url="https://public.gateway.example.com",
resource_base_url="https://public.gateway.example.com/mcp",
jwt_signing_key="test-secret-key",
client_storage=MemoryStore(),
)
assert str(proxy.issuer_url).rstrip("/") == "https://public.gateway.example.com"
assert proxy.resource_base_url is not None
assert (
str(proxy.resource_base_url).rstrip("/")
== "https://public.gateway.example.com/mcp"
)
async def test_refresh_expires_in_zero_subsequent_refresh_does_not_shrink(
self, proxy
):