Skip to content

Commit a144109

Browse files
committed
start
1 parent e360937 commit a144109

File tree

1 file changed

+327
-0
lines changed

1 file changed

+327
-0
lines changed

test_mcp_auth.py

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
"""
2+
Test Neo4j Aura MCP server OAuth flows and output Databricks configuration.
3+
4+
Flow:
5+
1. Register an OAuth client via Auth0 dynamic registration
6+
2. Run OAuth Authorization Code + PKCE flow (opens browser)
7+
3. Test the MCP endpoint with the user token
8+
4. Print Databricks configuration values
9+
10+
Key finding: The audience parameter must be OMITTED from the authorization
11+
request. Auth0 then defaults to audience=https://console.neo4j.io, which
12+
is what the MCP server validates against.
13+
"""
14+
15+
import base64
16+
import hashlib
17+
import http.server
18+
import json
19+
import os
20+
import secrets
21+
import sys
22+
import urllib.error
23+
import urllib.parse
24+
import urllib.request
25+
import webbrowser
26+
27+
# OAuth endpoints (discovered from https://mcp.neo4j.io/.well-known/oauth-authorization-server)
28+
AUTH0_ISSUER = "https://aura-mcp.eu.auth0.com"
29+
AUTHORIZATION_ENDPOINT = f"{AUTH0_ISSUER}/authorize"
30+
TOKEN_ENDPOINT = f"{AUTH0_ISSUER}/oauth/token"
31+
REGISTRATION_ENDPOINT = f"{AUTH0_ISSUER}/oidc/register"
32+
SCOPES = "openid profile email"
33+
34+
LOCAL_PORT = 8976
35+
REDIRECT_URI = f"http://localhost:{LOCAL_PORT}/callback"
36+
37+
38+
def load_env(path):
39+
"""Load .env file into a dict."""
40+
env = {}
41+
with open(path) as f:
42+
for line in f:
43+
line = line.strip()
44+
if not line or line.startswith("#"):
45+
continue
46+
key, _, value = line.partition("=")
47+
env[key.strip()] = value.strip().strip('"')
48+
return env
49+
50+
51+
def register_oauth_client(redirect_uri=None):
52+
"""Register a client via Auth0 dynamic client registration."""
53+
uri = redirect_uri or REDIRECT_URI
54+
payload = json.dumps({
55+
"client_name": "neo4j-mcp-databricks-test",
56+
"redirect_uris": [uri],
57+
"grant_types": ["authorization_code"],
58+
"response_types": ["code"],
59+
"token_endpoint_auth_method": "none",
60+
}).encode()
61+
62+
req = urllib.request.Request(
63+
REGISTRATION_ENDPOINT,
64+
data=payload,
65+
headers={"Content-Type": "application/json"},
66+
method="POST",
67+
)
68+
69+
print("1. Registering OAuth client via dynamic registration ...")
70+
print(f" Redirect URI: {uri}")
71+
try:
72+
with urllib.request.urlopen(req) as resp:
73+
body = json.loads(resp.read())
74+
client_id = body["client_id"]
75+
client_secret = body.get("client_secret", "")
76+
print(f" SUCCESS")
77+
print(f" Client ID: {client_id}")
78+
if client_secret:
79+
print(f" Client Secret: {client_secret}")
80+
return client_id, client_secret
81+
except urllib.error.HTTPError as e:
82+
print(f" FAILED - {e.code} {e.reason}")
83+
print(f" {e.read().decode()}")
84+
return None, None
85+
86+
87+
def generate_pkce():
88+
"""Generate PKCE code_verifier and code_challenge."""
89+
verifier = secrets.token_urlsafe(43)
90+
challenge = base64.urlsafe_b64encode(
91+
hashlib.sha256(verifier.encode()).digest()
92+
).rstrip(b"=").decode()
93+
return verifier, challenge
94+
95+
96+
def do_oauth_flow(client_id):
97+
"""Run OAuth Authorization Code + PKCE flow with local callback server.
98+
99+
IMPORTANT: Do NOT set the 'audience' parameter. Auth0 defaults to
100+
audience=https://console.neo4j.io, which the MCP server requires.
101+
Setting audience explicitly (e.g. to Auth0's /api/v2/) produces a
102+
token the MCP server rejects with 401.
103+
"""
104+
code_verifier, code_challenge = generate_pkce()
105+
state = secrets.token_urlsafe(16)
106+
107+
# Build authorization URL — NO audience parameter
108+
auth_params = urllib.parse.urlencode({
109+
"client_id": client_id,
110+
"redirect_uri": REDIRECT_URI,
111+
"response_type": "code",
112+
"scope": SCOPES,
113+
"state": state,
114+
"code_challenge": code_challenge,
115+
"code_challenge_method": "S256",
116+
# audience is intentionally omitted — Auth0 defaults to
117+
# https://console.neo4j.io which the MCP server expects
118+
})
119+
auth_url = f"{AUTHORIZATION_ENDPOINT}?{auth_params}"
120+
121+
# Capture the authorization code via local HTTP server
122+
result = {"code": None, "error": None}
123+
124+
class CallbackHandler(http.server.BaseHTTPRequestHandler):
125+
def do_GET(self):
126+
parsed = urllib.parse.urlparse(self.path)
127+
params = urllib.parse.parse_qs(parsed.query)
128+
129+
if "code" in params:
130+
result["code"] = params["code"][0]
131+
self.send_response(200)
132+
self.send_header("Content-Type", "text/html")
133+
self.end_headers()
134+
self.wfile.write(
135+
b"<h2>Authorization successful!</h2>"
136+
b"<p>You can close this tab and return to the terminal.</p>"
137+
)
138+
else:
139+
result["error"] = params.get("error", ["unknown"])[0]
140+
self.send_response(400)
141+
self.send_header("Content-Type", "text/html")
142+
self.end_headers()
143+
self.wfile.write(f"<h2>Error: {result['error']}</h2>".encode())
144+
145+
def log_message(self, format, *args):
146+
pass # Suppress server logs
147+
148+
server = http.server.HTTPServer(("localhost", LOCAL_PORT), CallbackHandler)
149+
150+
print(f"\n2. Starting OAuth Authorization Code + PKCE flow ...")
151+
print(f" Opening browser for Neo4j Aura login ...")
152+
print(f" (Log in with your Aura Console credentials)\n")
153+
154+
# Open browser in background
155+
webbrowser.open(auth_url)
156+
157+
# Wait for callback (single request)
158+
server.handle_request()
159+
server.server_close()
160+
161+
if result["error"]:
162+
print(f" FAILED - OAuth error: {result['error']}")
163+
return None
164+
165+
if not result["code"]:
166+
print(f" FAILED - No authorization code received")
167+
return None
168+
169+
print(f" Got authorization code, exchanging for token ...")
170+
171+
# Exchange code for token
172+
token_data = urllib.parse.urlencode({
173+
"grant_type": "authorization_code",
174+
"client_id": client_id,
175+
"code": result["code"],
176+
"redirect_uri": REDIRECT_URI,
177+
"code_verifier": code_verifier,
178+
}).encode()
179+
180+
req = urllib.request.Request(
181+
TOKEN_ENDPOINT,
182+
data=token_data,
183+
headers={"Content-Type": "application/x-www-form-urlencoded"},
184+
method="POST",
185+
)
186+
187+
try:
188+
with urllib.request.urlopen(req) as resp:
189+
body = json.loads(resp.read())
190+
token = body["access_token"]
191+
expires = body.get("expires_in", "unknown")
192+
print(f" SUCCESS - got user token (expires in {expires}s)")
193+
print(f" Token prefix: {token[:30]}...")
194+
195+
# Decode JWT to show audience (for debugging)
196+
if token.startswith("eyJ"):
197+
parts = token.split(".")
198+
payload_b64 = parts[1] + "=" * (4 - len(parts[1]) % 4)
199+
decoded = json.loads(base64.urlsafe_b64decode(payload_b64))
200+
print(f" Token audience: {decoded.get('aud')}")
201+
202+
return token
203+
except urllib.error.HTTPError as e:
204+
print(f" FAILED - {e.code} {e.reason}")
205+
print(f" {e.read().decode()}")
206+
return None
207+
208+
209+
def test_mcp_endpoint(mcp_url, token, label="3"):
210+
"""Send MCP initialize request to test auth."""
211+
payload = json.dumps({
212+
"jsonrpc": "2.0",
213+
"id": 1,
214+
"method": "initialize",
215+
"params": {
216+
"protocolVersion": "2025-03-26",
217+
"capabilities": {},
218+
"clientInfo": {"name": "databricks-auth-test", "version": "1.0.0"},
219+
},
220+
}).encode()
221+
222+
req = urllib.request.Request(
223+
mcp_url,
224+
data=payload,
225+
headers={
226+
"Authorization": f"Bearer {token}",
227+
"Content-Type": "application/json",
228+
"Accept": "application/json, text/event-stream",
229+
},
230+
method="POST",
231+
)
232+
233+
print(f"\n{label}. Testing MCP endpoint with user token ...")
234+
print(f" URL: {mcp_url}")
235+
try:
236+
with urllib.request.urlopen(req, timeout=30) as resp:
237+
body = resp.read().decode()
238+
print(f" SUCCESS - HTTP {resp.status}")
239+
print(f" Response: {body[:500]}")
240+
return True
241+
except urllib.error.HTTPError as e:
242+
body = e.read().decode()
243+
print(f" FAILED - HTTP {e.code} {e.reason}")
244+
print(f" Response: {body[:500]}")
245+
return False
246+
247+
248+
def print_databricks_config(client_id, client_secret, mcp_url):
249+
"""Print the values to enter in Databricks."""
250+
print("\n" + "=" * 60)
251+
print("DATABRICKS CONFIGURATION")
252+
print("=" * 60)
253+
print()
254+
print("Step 1 - Connection basics:")
255+
print(f" Connection name: finance_mcp_server")
256+
print(f" Connection type: HTTP")
257+
print(f" Auth type: OAuth User to Machine Per User")
258+
print(f" OAuth provider: Manual configuration")
259+
print()
260+
print("Step 2 - Authentication:")
261+
print(f" Host: https://mcp.neo4j.io")
262+
print(f" Port: 443")
263+
print(f" OAuth scope: {SCOPES}")
264+
print(f" Client ID: {client_id}")
265+
print(f" Client secret: {client_secret or '(leave empty - public client)'}")
266+
print(f" Authorization endpoint: {AUTHORIZATION_ENDPOINT}")
267+
print(f" Token endpoint: {TOKEN_ENDPOINT}")
268+
print()
269+
print("IMPORTANT: Do NOT set an OAuth audience parameter.")
270+
print("Auth0 must default to audience=https://console.neo4j.io")
271+
print("for the MCP server to accept the token.")
272+
print()
273+
print("MCP Server URL (for Databricks MCP tool config):")
274+
print(f" {mcp_url}")
275+
print()
276+
print("NOTE: The client_id above was registered with redirect_uri")
277+
print(f" {REDIRECT_URI}")
278+
print("If Databricks uses a different redirect_uri, re-register via:")
279+
print(f" POST {REGISTRATION_ENDPOINT}")
280+
print(f" Body: {{\"client_name\":\"databricks\",\"redirect_uris\":[\"<DATABRICKS_URI>\"],")
281+
print(f" \"grant_types\":[\"authorization_code\"],\"response_types\":[\"code\"],")
282+
print(f" \"token_endpoint_auth_method\":\"none\"}}")
283+
print()
284+
285+
286+
def main():
287+
env_path = os.path.join(os.path.dirname(__file__), "financial_data_load", ".env")
288+
print(f"Loading credentials from {env_path}\n")
289+
env = load_env(env_path)
290+
291+
mcp_url = env.get("AURA_AGENT_MCP_SERVER")
292+
if not mcp_url:
293+
print("ERROR: Missing AURA_AGENT_MCP_SERVER in .env")
294+
sys.exit(1)
295+
296+
# Step 1: Register OAuth client
297+
client_id, client_secret = register_oauth_client()
298+
if not client_id:
299+
print("\nCannot proceed without a client ID.")
300+
sys.exit(1)
301+
302+
# Step 2: OAuth flow (opens browser)
303+
token = do_oauth_flow(client_id)
304+
if not token:
305+
print("\nCannot proceed without a token.")
306+
sys.exit(1)
307+
308+
# Step 3: Test MCP endpoint
309+
mcp_ok = test_mcp_endpoint(mcp_url, token)
310+
311+
# Print Databricks config
312+
print_databricks_config(client_id, client_secret, mcp_url)
313+
314+
# Summary
315+
print("=" * 60)
316+
print("RESULT")
317+
print("=" * 60)
318+
if mcp_ok:
319+
print(" User OAuth token WORKS with MCP endpoint!")
320+
print(" OAuth User to Machine Per User should work in Databricks.")
321+
else:
322+
print(" User OAuth token FAILED with MCP endpoint.")
323+
print(" Further investigation needed.")
324+
325+
326+
if __name__ == "__main__":
327+
main()

0 commit comments

Comments
 (0)