Description
Background
Combining these facts:
- Jupyter Servers (often) use API tokens for authentication
- auth tokens are typically passed in the Authorization header
- websockets cannot pass tokens in Authorization headers because the browser implementation forbids it, though most implementations outside the browser do support it.
As a result, websocket requests must pass tokens in a URL parameter. - Passing tokens in a URL parameter is generally frowned upon, but not strictly insecure and indeed explicitly recommended by Browser websocket implementers
motivates having a new mechanism by which to pass the auth token for websocket requests that's not in the URL.
Proposal
Add support for passing authentication tokens in the websocket subprotocol for Jupyter Server, to avoid tokens in URL parameters.
This is a scheme devised by Kubernetes, where the subprotocols API allows specifying the Sec-Websocket-Protocol header, and we can put the token in there.
Example implementation
(draft implementation of this at jupyter-server/jupyter_server#1407)
To authenticate websockets via new mechanism, clients should:
ws = new WebSocket(wss://..., ['v1.token.websocket.jupyter.org', `v1.token.websocket.jupyter.org.${token}`, ...])
which sets the header:
Sec-WebSocket-Protocol: v1.token.websocket.jupyter.org, v1.token.websocket.jupyter.org.abc123
The response will have the header:
Sec-WebSocket-Protocol: v1.token.websocket.jupyter.org
The reason for the double subprotocol is that if any subprotocol is requested, the response must include one of the requested subprotocols for the connection to be accepted by all browsers. Not all browser require this, but Chrome does. The v1.token.websocket.jupyter.org
serves no purpose if there is already a subprotocol defined and required, and should be optional.
Remaining questions
The main implementation question is how should frontends negotiate/discover this feature. We have already done it once in jupyterlab/jupyterlab#11841 adding the kernel subprotocol, and the solution was to make no assumptions and try once with subprotocols, and then reconnect without them if it fails. This becomes a little bit more complicated when there are two subprotocols that may not be implemented, but the same strategy is valid and requires no prior knowledge of the server's support for the new scheme.
Why a JEP
This is a JEP because it affects a relatively low-level feature in the Jupyer Server that clients (JupyterLab) should implement. It is fully backward-compatible, so perhaps it doesn't need to be, but it does involve implementation in both jupyter-server and jupyterlab. I have a proposed implementation already. I'd be happy for this to just be a new feature in Jupyter Server that future frontends can implement as needed. There is no plan to deprecate the existing behavior, so there is relatively little pressure to implement by frontends, as nothing can be broken by this.
There are also multiple possible ways to go about this, such as a handshake message, which has a lot of disadvantages, as I see it (backward-compatiblity being much harder, for one).
Affected subprojects:
- jupyter server implementations (jupyter-server, jupyverse)
- jupyter server clients (JupyterLab, maybe some server proxies like enterprise gateway?)
Recommended reviewers:
- from Jupyter Server (e.g. @Zsailer, @davidbrochart)
- from frontends (@jtpio)
- from security (@rpwagner)