feat: add password-based authentication support - #250
Conversation
should this be fixed in upstream jupyter-nbmodel-client? not sure how this would help this PR? |
|
@echarles It would allow us to get rid of this not great piece of code: def _connect_with_auth(uri, **kwargs):
kwargs.setdefault("additional_headers", {})
kwargs["additional_headers"]["Cookie"] = cookie_header
return original_connect(uri, **kwargs)
nbmodel_client_module.connect = _connect_with_authin But if we are fine with it, we can keep it as is. But it's somewhat fragile if the |
As we are talking about security, very clear explanations would be useful. Is it possible to create a security section / page in the docs, and explain the various ways and implications the jupyter mcp server can be configured, included those changes. |
|
I wonder if the tests run against the real instance of a jupyter server or the test code only test a standalone instance of the newly added class mean to implement password based auth? |
|
@abbbe i tested this implementation with claude code agains real jupyter lab server multiple times |
|
I am not speaking on behalf of the project here, mere a party interested in the feature, but “works for me” is not quite the same as proper CI. The project test suite already has all the machinery to spawn Jupiter Lab server etc, it is not too complicated to integrate your tests. |
|
@echarles Good point — I've added documentation for the password auth feature Security docs (docs/docs/reference/security/index.mdx):
Configuration docs (docs/docs/reference/configuration/index.mdx):
Let me know if you'd like any changes to the structure or content. |
|
@EnyMan this is a very cool feature, actually wanted to implement it myself but never could wrap my head around it. I am happy to try to review it, but would you be open to do the following to begin with:
|
I think this (extending nbmodel client) is the best approach, all this monkey patching business is rather sketchy. |
|
Better test are on the way |
ef07222 to
53121d4
Compare
|
@abbbe Thanks for looking at this I did several rounds against grumpy opus reviewer. |
|
@abbbe Any further comment before I review this? |
|
I will have a look todaySent from my iPhoneOn 29 May 2026, at 09:38, Eric Charles ***@***.***> wrote:echarles left a comment (datalayer/jupyter-mcp-server#250)
@abbbe Any further comment before I review this?
—Reply to this email directly, view it on GitHub, or unsubscribe.Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
|
There is something else. The current implementation logs in once, but does not retry if session goes stale (cookie expires) throughout the session. I understand by default jupyter cookies expire in 30 days and normal desktop users are unlikely to be affected, but long running sessions might fail. I did not investigate this in details, because things might change depending on how the race condition will be fixed. Something to look into... |
|
Also, I think setting token to blank value is not useful but create risks. Not useful, because password-based authentication gives access to API even without IdentityProvider.token="". Dangerous, because if things go wrong with the password based authentication (for whatever reason), user ends up with zero authentication. |
|
Thanks for the review i should be able to look at it sometime this week. |
|
@abbbe Good catch on the Jupyter cookie expiration i haven't thought about it. As for the implementation of it i plan to look for 401/403 an re-auth. By any chance do you know if the collaboration WebSocket surfaces this cleanly? I am also going to wait for the |
My guess is web socket is not affected, in a sense that once it is open with a valid cookie, the web socket itself just stays open even if the cookie used to open it has expired. |
|
@abbbe Removed the monkey-patch and I added the re-login flow to the collaboration/document path. Do you want it added also to the Kernel management path |
|
While testing this branch I hit a bug: |
|
@EnyMan Is this ready for review? |
Yes, it is. Let me rebase to master really quickly, ok it might not be that quick since the CLI changes |
df44208 to
ff38f60
Compare
|
Fixing the test plus some new additions that were since added to main. |
|
thx, are the items listed in the first note still opened? |
The last one is done. For the first two, I can either test manually or try to write tests for them. The manual test might need to wait until tomorrow. |
|
OK verified the two last two points |
1f5b316 to
68b9ce8
Compare
|
Just merged #306 and release V 1.1.0 which has internal breaking changes, no changes for the users. The logic is to use the code-sandboxes package instead of the jupyter-kernel-client, this has introduced conflict on your PR, sorry for that! |
68b9ce8 to
007ef39
Compare
007ef39 to
cbcd2dc
Compare
|
Rebased and opened up datalayer/code-sandboxes#13; it's the same kind of issue as with the nb-client. |
Great, merged and code sandboxes 0.17.0 release with that
#328 is merged and pins mcp<2 - new release on its way. |
Add support for authenticating against password-protected Jupyter servers, in addition to token/anonymous auth. - auth.py: log in to /login, capture the XSRF token and session cookie, and expose auth headers/cookies for HTTP, WebSocket, and collaboration API requests; re-authenticate on 401/403 when the session cookie expires and use a fresh cookie for the WebSocket handshake after relogin. - Inject the X-XSRFToken header on notebook-create and other POST requests when using password (cookie) auth. - Thread auth headers into the kernel client (create_kernel and the execute_code cross-kernel connection) and the collaboration WebSocket (NbModelClient additional_headers); drop the token when password auth supplies cookie/XSRF headers so it can't override them. - CLI/config: add --runtime-password / --document-password / --jupyter-password options (with matching env vars) that fall back to the shared --jupyter-password and take precedence over the tokens. - Docs: document password authentication in the security and configuration references. - Tests: unit tests (test_auth.py) plus e2e tests against a real password-protected Jupyter server; the e2e read-back polls the collaboration YDoc to tolerate its eventual consistency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cbcd2dc to
4f26f2b
Compare
|
Rebased and pushed. So unless something breaks again, ready to review @echarles |
ServerContext only ever fetches the `_xsrf` cookie when a code sandbox password is configured (PR datalayer#250). When neither a password nor a token is set, code_sandbox_auth_headers stays permanently {}, so every state-changing request (kernel management, collaboration API) fails with "'_xsrf' argument missing from POST" on deployments where auth is handled externally: --IdentityProvider.token='', JupyterHub single-user servers, or managed environments (SageMaker Studio, Colab Enterprise). Add JupyterAnonymousAuth, a JupyterPasswordAuth subclass that performs only the anonymous GET /login step to pick up the _xsrf cookie, skipping the password POST and verification. Wire it into _init_mcp_server_mode's existing no-password branch when code_sandbox_token is also unset. Every reader of _code_sandbox_password_auth (code_sandbox_auth_headers, relogin_code_sandbox, document auth reuse) already dispatches through get_headers()/relogin()/close(), so no other call site needed to change. Scoped to the code sandbox / shared-URL case reported in the issue; a document server configured at a different URL with no credentials of its own is unchanged (still silently unauthenticated, same as before). Fixes datalayer#183
…et (#333) * fix(auth): fetch anonymous XSRF cookie when no password or token is set ServerContext only ever fetches the `_xsrf` cookie when a code sandbox password is configured (PR #250). When neither a password nor a token is set, code_sandbox_auth_headers stays permanently {}, so every state-changing request (kernel management, collaboration API) fails with "'_xsrf' argument missing from POST" on deployments where auth is handled externally: --IdentityProvider.token='', JupyterHub single-user servers, or managed environments (SageMaker Studio, Colab Enterprise). Add JupyterAnonymousAuth, a JupyterPasswordAuth subclass that performs only the anonymous GET /login step to pick up the _xsrf cookie, skipping the password POST and verification. Wire it into _init_mcp_server_mode's existing no-password branch when code_sandbox_token is also unset. Every reader of _code_sandbox_password_auth (code_sandbox_auth_headers, relogin_code_sandbox, document auth reuse) already dispatches through get_headers()/relogin()/close(), so no other call site needed to change. Scoped to the code sandbox / shared-URL case reported in the issue; a document server configured at a different URL with no credentials of its own is unchanged (still silently unauthenticated, same as before). Fixes #183 * fix(auth): don't let an unreachable server abort MCP_SERVER init The anonymous XSRF fetch added in the previous commit ran eagerly inside _init_mcp_server_mode, so any config with no token and no password (the process-global default many unrelated tests and code paths construct) now performs a real network GET at ServerContext init time. A server that isn't reachable yet turned that into a RuntimeError that aborted startup, where before this feature existed the same config did zero network I/O. Extract the login attempt into _try_anonymous_auth, which returns the auth object either way (inject_into_session/get_headers already no-op when _authenticated is False), and degrade to no cookie on a connection error instead of raising. Explicit password auth is unaffected: a user who configured a password still gets a hard failure if login fails. Caught this via CI on the open PR (6 previously-passing tests failed across the full matrix); reproduced locally and confirmed the same 6 pass again after this change, with no new ruff findings. Signed-off-by: Amir Fathi <amirfathi.me@gmail.com> --------- Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
Summary
Adds password-based authentication as an alternative to token auth, enabling the MCP server to work with Jupyter deployments that use password login and XSRF protection.
Related issues: Partially addresses #183 (XSRF-protected Jupyter deployments without Bearer tokens), related to #61 (WebSocket authentication failures)
Changes
JupyterPasswordAuthclass (auth.py) handles the/loginflow: obtains XSRF cookie, POSTs credentials, verifies the session, and keeps arequests.Sessionalive so subsequent callers see live cookie values rather than a login-time snapshot.--runtime-password,--document-password,--jupyter-password(with corresponding env varsRUNTIME_PASSWORD,DOCUMENT_PASSWORD,JUPYTER_PASSWORD) follow the same priority/fallback pattern as existing token options. Password takes precedence over token when both are set for the same server.JupyterServerClient,KernelClient, and the collaboration WebSocket connection. The freshX-XSRFTokenheader is read per-request from the live cookie jar, so cookie rotation is handled correctly._runtime_password_authfor kernel operations and_document_password_authfor collaboration. Whenruntime_url == document_urlthe runtime auth is reused; when URLs differ and--document-passwordis set, a second login is performed. A warning is logged when the document server is genuinely different and only--runtime-passwordis set.jupyter-nbmodel-client's nativeadditional_headers(NbModelClient) andheaders(get_notebook_websocket_url) parameters to inject the Cookie/XSRF auth — see "Removed monkey-patch" below.NotebookConnectionnow catches401/403on the collaboration-session request, re-authenticates (ServerContext.relogin_document()→JupyterPasswordAuth.relogin()), re-injects the fresh cookies, and retries once._init_mcp_server_modelogic inserver_context.pyinto a single method with proper resource cleanup on partial failure.ServerContext.reset()now closes auth sessions to release connection pools._remote_connection_options(the subset thatconnectcan forward over/api/connect) and_connection_options(adds local-only password options used bystart). This also fixed a pre-existingTypeErrorinconnect_commandonmain.test_auth.py) covering the login flow (success, timeout, connection error at each stage, 5xx, 401/403, missing-xsrf, unexpected-status, response-body propagation), header generation, session injection, config normalization, dual runtime/document auth scenarios, session expiry / re-login, and e2e tests against a real password-protected Jupyter server.How it works
ServerContextcreates aJupyterPasswordAuthinstance and calls.login()to authenticate.JupyterServerClient's HTTP session, and the auth's ownrequests.Sessionis kept alive so cookies remain readable for the document side.KernelClientand collaboration API requests, auth headers (Cookie+X-XSRFToken) are passed via theheaderskwarg and computed per-request from the live cookie jar.runtime_auth_headersanddocument_auth_headersproperties onServerContextalways read the latest cookies from the active session (not the stale login-time snapshot).401/403(expired cookie), the connection re-authenticates and retries once with the fresh cookies.Removed monkey-patch
Earlier revisions of this PR monkey-patched the module-level
connectimported injupyter_nbmodel_client.clientto inject the Cookie header during the WebSocket handshake, becauseNbModelClientexposed no way to pass extra headers. That patch was not concurrency-safe.This is now resolved upstream: jupyter-nbmodel-client#61 added
additional_headers=toNbModelClientandheaders=toget_notebook_websocket_url(released injupyter-nbmodel-client 0.14.8). The monkey-patch and the re-implemented URL helper have been removed; the minimum dependency is bumped to>=0.14.8.Known limitation
The automatic re-login currently covers the collaboration / document path (
NotebookConnection). Kernel-management HTTP requests that go throughjupyter_server_clientinternals are not yet wrapped in the same retry; this can be a follow-up.Test plan
uv run --extra test pytest tests/test_auth.pypasses (unit + e2e, including session-expiry/re-login tests against a real password-protected Jupyter server)jupyter lab --IdentityProvider.token='' --ServerApp.password='argon2:...'--jupyter-passwordfalls back correctly when individual passwords are not setjupyter-mcp-server connect --helpshows no--*-passwordoptions (passwords are local-only; not forwardable over/api/connect)