Skip to content

Commit 8c1bae0

Browse files
committed
handle api tokens and xsrf
- XSRF checks are applied to GET requests, blocking the EventSource - switch to fetch-based EventSource implementation, since base EventSource doesn't allow headers (like websockets) - propagate api tokens for authenticated requests
1 parent 8a8a59e commit 8c1bae0

File tree

8 files changed

+75
-29
lines changed

8 files changed

+75
-29
lines changed

binderhub/base.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,19 @@ def get_current_user(self):
137137

138138
@property
139139
def template_namespace(self):
140-
return dict(
140+
141+
ns = dict(
141142
static_url=self.static_url,
142143
banner=self.settings["banner_message"],
143-
**self.settings.get("template_variables", {}),
144+
auth_enabled=self.settings["auth_enabled"],
145+
)
146+
if self.settings["auth_enabled"]:
147+
ns["api_token"] = self.hub_auth.get_token(self) or ""
148+
149+
ns.update(
150+
self.settings.get("template_variables", {}),
144151
)
152+
return ns
145153

146154
def set_default_headers(self):
147155
headers = self.settings.get("headers", {})

binderhub/builder.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ def _get_build_only(self):
247247

248248
return build_only
249249

250+
def redirect(self, *args, **kwargs):
251+
# disable redirect to login, which won't work for EventSource
252+
raise HTTPError(403)
253+
250254
@authenticated
251255
async def get(self, provider_prefix, _unescaped_spec):
252256
"""Get a built image for a given spec and repo provider.

binderhub/static/js/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/* If this file gets over 200 lines of code long (not counting docs / comments), start using a framework
22
*/
33
import ClipboardJS from "clipboard";
4-
import "event-source-polyfill";
54

65
import { BinderRepository } from "@jupyterhub/binderhub-client";
76
import { updatePathText } from "./src/path";
@@ -61,11 +60,14 @@ async function build(providerSpec, log, fitAddon, path, pathType) {
6160
$(".on-build").removeClass("hidden");
6261

6362
const buildToken = $("#build-token").data("token");
63+
let apiToken = $("#api-token").data("token");
6464
const buildEndpointUrl = new URL("build", BASE_URL);
6565
const image = new BinderRepository(
6666
providerSpec,
6767
buildEndpointUrl,
6868
buildToken,
69+
false,
70+
{ apiToken },
6971
);
7072

7173
for await (const data of image.fetch()) {

binderhub/templates/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
{% block head %}
44
<meta id="base-url" data-url="{{base_url}}">
55
<meta id="badge-base-url" data-url="{{badge_base_url}}">
6+
<meta id="api-token" data-token="{{ api_token }}">
67
<script src="{{static_url("dist/bundle.js")}}"></script>
78
{{ super() }}
89
{% endblock head %}

binderhub/templates/loading.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<meta id="base-url" data-url="{{base_url}}">
1515
<meta id="badge-base-url" data-url="{{badge_base_url}}">
1616
<meta id="build-token" data-token="{{ build_token }}">
17+
<meta id="api-token" data-token="{{ api_token }}">
1718
{{ super() }}
1819
<script src="{{static_url("dist/bundle.js")}}"></script>
1920
<link href="{{static_url("loading.css")}}" rel="stylesheet">

js/packages/binderhub-client/lib/index.js

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1-
import { NativeEventSource, EventSourcePolyfill } from "event-source-polyfill";
1+
import { fetchEventSource } from "@microsoft/fetch-event-source";
22
import { EventIterator } from "event-iterator";
33

4-
// Use native browser EventSource if available, and use the polyfill if not available
5-
const EventSource = NativeEventSource || EventSourcePolyfill;
6-
4+
function _getXSRFToken() {
5+
// from @jupyterlab/services
6+
let cookie = "";
7+
try {
8+
cookie = document.cookie;
9+
} catch (e) {
10+
// e.g. SecurityError in case of CSP Sandbox
11+
return null;
12+
}
13+
const xsrfTokenMatch = cookie.match("\\b_xsrf=([^;]*)\\b");
14+
if (xsrfTokenMatch) {
15+
return xsrfTokenMatch[1];
16+
}
17+
return null;
18+
}
719
/**
820
* Build (and optionally launch) a repository by talking to a BinderHub API endpoint
921
*/
@@ -14,8 +26,15 @@ export class BinderRepository {
1426
* @param {URL} buildEndpointUrl API URL of the build endpoint to talk to
1527
* @param {string} [buildToken] Optional JWT based build token if this binderhub installation requires using build tokens
1628
* @param {boolean} [buildOnly] Opt out of launching built image by default by passing `build_only` param
29+
* @param {string} [apiToken] Optional Bearer token for authenticating requests
1730
*/
18-
constructor(providerSpec, buildEndpointUrl, buildToken, buildOnly) {
31+
constructor(
32+
providerSpec,
33+
buildEndpointUrl,
34+
buildToken,
35+
buildOnly,
36+
{ apiToken },
37+
) {
1938
this.providerSpec = providerSpec;
2039
// Make sure that buildEndpointUrl is a real URL - this ensures hostname is properly set
2140
if (!(buildEndpointUrl instanceof URL)) {
@@ -40,6 +59,7 @@ export class BinderRepository {
4059
if (buildOnly) {
4160
this.buildUrl.searchParams.append("build_only", "true");
4261
}
62+
this.apiToken = apiToken;
4363

4464
this.eventIteratorQueue = null;
4565
}
@@ -67,26 +87,37 @@ export class BinderRepository {
6787
* @returns {AsyncIterable<Line>} An async iterator yielding responses from the API as they come in
6888
*/
6989
fetch() {
70-
this.eventSource = new EventSource(this.buildUrl);
71-
return new EventIterator((queue) => {
90+
const headers = {};
91+
if (this.apiToken && this.apiToken.length > 0) {
92+
headers["Authorization"] = `Bearer ${this.apiToken}`;
93+
} else {
94+
const xsrf = _getXSRFToken();
95+
if (xsrf) {
96+
headers["X-Xsrftoken"] = xsrf;
97+
}
98+
}
99+
return new EventIterator(async (queue) => {
72100
this.eventIteratorQueue = queue;
73-
this.eventSource.onerror = () => {
74-
queue.push({
75-
phase: "failed",
76-
message: "Failed to connect to event stream\n",
77-
});
78-
queue.stop();
79-
};
80-
81-
this.eventSource.addEventListener("message", (event) => {
82-
// console.log("message received")
83-
// console.log(event)
84-
const data = JSON.parse(event.data);
85-
// FIXME: fix case of phase/state upstream
86-
if (data.phase) {
87-
data.phase = data.phase.toLowerCase();
88-
}
89-
queue.push(data);
101+
await fetchEventSource(this.buildUrl, {
102+
headers,
103+
onerror: () => {
104+
queue.push({
105+
phase: "failed",
106+
message: "Failed to connect to event stream\n",
107+
});
108+
queue.stop();
109+
},
110+
111+
onmessage: (event) => {
112+
// console.log("message received")
113+
// console.log(event)
114+
const data = JSON.parse(event.data);
115+
// FIXME: fix case of phase/state upstream
116+
if (data.phase) {
117+
data.phase = data.phase.toLowerCase();
118+
}
119+
queue.push(data);
120+
},
90121
});
91122
});
92123
}

js/packages/binderhub-client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"homepage": "https://github.com/jupyterhub/binderhub#readme",
1616
"dependencies": {
17-
"event-source-polyfill": "^1.0.31",
17+
"@microsoft/fetch-event-source": "^2.0.1",
1818
"event-iterator": "^2.0.0"
1919
}
2020
}

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"dependencies": {
55
"bootstrap": "^3.4.1",
66
"clipboard": "^2.0.11",
7-
"event-source-polyfill": "^1.0.31",
87
"jquery": "^3.6.4",
98
"xterm": "^5.1.0",
109
"xterm-addon-fit": "^0.7.0"

0 commit comments

Comments
 (0)