diff --git a/fastmcp_slim/fastmcp/server/auth/providers/keycloak.py b/fastmcp_slim/fastmcp/server/auth/providers/keycloak.py index dd874dcbf..5dcb0b3e6 100644 --- a/fastmcp_slim/fastmcp/server/auth/providers/keycloak.py +++ b/fastmcp_slim/fastmcp/server/auth/providers/keycloak.py @@ -132,6 +132,7 @@ class KeycloakOAuthProxy(OAuthProxy): upstream_authorization_endpoint: str | None = None, upstream_token_endpoint: str | None = None, upstream_revocation_endpoint: str | None = None, + redirect_path: str | None = None, # Pass-through OAuthProxy options jwt_signing_key: str | bytes | None = None, client_storage: AsyncKeyValue | None = None, @@ -162,6 +163,11 @@ class KeycloakOAuthProxy(OAuthProxy): upstream_token_endpoint: Override the token endpoint URL. Required if `realm_url` is not provided. upstream_revocation_endpoint: Optional token revocation endpoint. + redirect_path: Callback path registered with the upstream Keycloak + client. Defaults to `/auth/callback`. Set this when migrating an + existing deployment whose Keycloak client uses a non-default + callback path, so the authorization request keeps matching the + registered redirect URI. jwt_signing_key: Secret for signing FastMCP JWTs. client_storage: Storage backend for OAuth state. require_authorization_consent: Consent screen behaviour (default True). @@ -216,6 +222,7 @@ class KeycloakOAuthProxy(OAuthProxy): upstream_client_secret=upstream_client_secret, token_verifier=token_verifier, valid_scopes=parsed_valid_scopes, + redirect_path=redirect_path, base_url=base_url, jwt_signing_key=jwt_signing_key, client_storage=client_storage, diff --git a/tests/server/auth/oauth_proxy/test_tokens.py b/tests/server/auth/oauth_proxy/test_tokens.py index 68f200937..7d6f7c479 100644 --- a/tests/server/auth/oauth_proxy/test_tokens.py +++ b/tests/server/auth/oauth_proxy/test_tokens.py @@ -968,6 +968,33 @@ class TestKeycloakOAuthProxy: "offline_access", ] + def test_redirect_path_defaults_to_auth_callback(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 proxy._redirect_path == "/auth/callback" + + def test_redirect_path_forwarded_for_custom_callback(self, jwt_verifier): + """Migrating a deployment whose Keycloak client uses a non-default + callback path must not silently revert to /auth/callback.""" + 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", + redirect_path="/custom/oauth/callback", + jwt_signing_key="test-secret-key", + client_storage=MemoryStore(), + ) + assert proxy._redirect_path == "/custom/oauth/callback" + async def test_refresh_expires_in_zero_subsequent_refresh_does_not_shrink( self, proxy ):