diff --git a/examples/auth-provider-local-mcp/uv.lock b/examples/auth-provider-local-mcp/uv.lock index 99cc948..806ca2d 100644 --- a/examples/auth-provider-local-mcp/uv.lock +++ b/examples/auth-provider-local-mcp/uv.lock @@ -706,7 +706,7 @@ wheels = [ [[package]] name = "north-mcp-python-sdk" -version = "0.2.5" +version = "0.3.0" source = { editable = "../../" } dependencies = [ { name = "fastmcp" }, @@ -715,7 +715,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "fastmcp", specifier = ">=2.14.5" }, + { name = "fastmcp", specifier = ">=2.14.5,<3" }, { name = "pyjwt", extras = ["crypto"], specifier = ">=2.10.1" }, ] diff --git a/pyproject.toml b/pyproject.toml index 117fe5d..a361f55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "north-mcp-python-sdk" -version = "0.2.5" +version = "0.3.0" description = "Add your description here" readme = "README.md" authors = [{ name = "Raphael Cristal", email = "raphael@cohere.com" }] requires-python = ">=3.11" dependencies = [ - "fastmcp>=2.14.5", + "fastmcp>=2.14.5,<3", "pyjwt[crypto]>=2.10.1", ] diff --git a/src/north_mcp_python_sdk/auth.py b/src/north_mcp_python_sdk/auth.py index 57aba05..f4d75e6 100644 --- a/src/north_mcp_python_sdk/auth.py +++ b/src/north_mcp_python_sdk/auth.py @@ -222,7 +222,7 @@ def __init__( def _has_x_north_headers(self, conn: HTTPConnection) -> bool: """Check if any X-North headers are present.""" return any( - header in conn.headers and conn.headers[header].strip() != "" + header in conn.headers for header in [ "X-North-ID-Token", "X-North-Connector-Tokens", @@ -273,9 +273,7 @@ def _validate_server_secret(self, provided_secret: str | None) -> None: DeprecationWarning, ) - if ( - self._server_secret and self._server_secret != provided_secret - ) or (not self._server_secret and provided_secret): + if self._server_secret and self._server_secret != provided_secret: self.logger.debug("Server secret mismatch - access denied") raise AuthenticationError("access denied") @@ -352,24 +350,10 @@ async def _authenticate_x_north_headers( self.logger.debug("Using X-North headers for authentication") # Extract headers - # Auth Headers user_id_token = conn.headers.get("X-North-ID-Token") - server_secret = conn.headers.get("X-North-Server-Secret") - - if not user_id_token and not server_secret: - self.logger.debug( - "No X-North-ID-Token or X-North-Server-Secret header present" - ) - raise AuthenticationError("no authentication headers present") - - self._validate_server_secret(server_secret) - token_email = self._process_user_id_token(user_id_token) - - self.logger.debug("X-North authentication successful") - - # Additional Headers - connector_tokens_header = conn.headers.get("X-North-Connector-Tokens") user_email_header = conn.headers.get("X-North-User-Email") + connector_tokens_header = conn.headers.get("X-North-Connector-Tokens") + server_secret = conn.headers.get("X-North-Server-Secret") # Parse connector tokens (Base64 URL-safe encoded JSON) connector_access_tokens = {} @@ -382,10 +366,6 @@ async def _authenticate_x_north_headers( connector_tokens_header ) - email = token_email - if token_email is None and user_email_header: - email = user_email_header - self.logger.debug( "X-North headers parsed. Has server_secret: %s, Has user_id_token: %s, Connector count: %d", server_secret is not None and server_secret != "", @@ -396,6 +376,12 @@ async def _authenticate_x_north_headers( "Available connectors: %s", list(connector_access_tokens.keys()) ) + self._validate_server_secret(server_secret) + email = self._process_user_id_token(user_id_token) + if email is None and user_email_header: + email = user_email_header + + self.logger.debug("X-North authentication successful") return self._create_authenticated_user( email, connector_access_tokens, user_id_token ) diff --git a/tests/test_x_north_auth_backend.py b/tests/test_x_north_auth_backend.py index dcd37e2..6237f71 100644 --- a/tests/test_x_north_auth_backend.py +++ b/tests/test_x_north_auth_backend.py @@ -284,24 +284,6 @@ async def test_x_north_empty_headers_treated_as_absent(): assert isinstance(user, AuthenticatedUser) -@pytest.mark.asyncio -async def test_x_north_whitespace_only_headers_treated_as_absent(): - """Test that whitespace-only X-North headers are treated as absent.""" - backend = NorthAuthBackend() - - # Whitespace-only should be treated as absent - headers = { - "X-North-ID-Token": " ", - "X-North-Server-Secret": " ", - "X-North-Connector-Tokens": " ", - } - conn = create_mock_connection(headers) - - # Should fall back to legacy auth since X-North headers are effectively absent - with pytest.raises(AuthenticationError, match="invalid authorization"): - await backend.authenticate(conn) - - @pytest.mark.asyncio async def test_no_auth_headers_present(): """Test error when no authentication headers are provided at all.""" @@ -406,21 +388,3 @@ async def test_connector_tokens_non_dict_ignored(): # Non-dict values result in empty connector tokens assert user.access_token.claims["connector_access_tokens"] == {} - - -@pytest.mark.asyncio -async def test_server_sends_secret_when_none_expected(): - """Test error when client sends secret but server doesn't expect one.""" - backend = NorthAuthBackend(server_secret=None) # No secret expected - - user_id_token = jwt.encode( - payload={"email": "test@company.com"}, key="test" - ) - headers = { - "X-North-ID-Token": user_id_token, - "X-North-Server-Secret": "unexpected_secret", # Server doesn't expect this - } - conn = create_mock_connection(headers) - - with pytest.raises(AuthenticationError, match="access denied"): - await backend.authenticate(conn) diff --git a/uv.lock b/uv.lock index 7ae1943..8328f53 100644 --- a/uv.lock +++ b/uv.lock @@ -838,7 +838,7 @@ wheels = [ [[package]] name = "north-mcp-python-sdk" -version = "0.2.5" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "fastmcp" }, @@ -857,7 +857,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "fastmcp", specifier = ">=2.14.5" }, + { name = "fastmcp", specifier = ">=2.14.5,<3" }, { name = "pyjwt", extras = ["crypto"], specifier = ">=2.10.1" }, ]