Skip to content
Closed
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
4 changes: 2 additions & 2 deletions examples/auth-provider-local-mcp/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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",
]

Expand Down
34 changes: 10 additions & 24 deletions src/north_mcp_python_sdk/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
JoshBragg-Cohere marked this conversation as resolved.
for header in [
"X-North-ID-Token",
"X-North-Connector-Tokens",
Expand Down Expand Up @@ -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):
Comment thread
cursor[bot] marked this conversation as resolved.
if self._server_secret and self._server_secret != provided_secret:
self.logger.debug("Server secret mismatch - access denied")
raise AuthenticationError("access denied")

Expand Down Expand Up @@ -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 = {}
Expand All @@ -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 != "",
Expand All @@ -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
)
Expand Down
36 changes: 0 additions & 36 deletions tests/test_x_north_auth_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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)
4 changes: 2 additions & 2 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.