Skip to content

Commit

Permalink
handle api tokens and xsrf
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
minrk committed May 3, 2024
1 parent 8a8a59e commit 8c1bae0
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 29 deletions.
12 changes: 10 additions & 2 deletions binderhub/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,19 @@ def get_current_user(self):

@property
def template_namespace(self):
return dict(

ns = dict(
static_url=self.static_url,
banner=self.settings["banner_message"],
**self.settings.get("template_variables", {}),
auth_enabled=self.settings["auth_enabled"],
)
if self.settings["auth_enabled"]:
ns["api_token"] = self.hub_auth.get_token(self) or ""

ns.update(
self.settings.get("template_variables", {}),
)
return ns

def set_default_headers(self):
headers = self.settings.get("headers", {})
Expand Down
4 changes: 4 additions & 0 deletions binderhub/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ def _get_build_only(self):

return build_only

def redirect(self, *args, **kwargs):
# disable redirect to login, which won't work for EventSource
raise HTTPError(403)

@authenticated
async def get(self, provider_prefix, _unescaped_spec):
"""Get a built image for a given spec and repo provider.
Expand Down
4 changes: 3 additions & 1 deletion binderhub/static/js/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* If this file gets over 200 lines of code long (not counting docs / comments), start using a framework
*/
import ClipboardJS from "clipboard";
import "event-source-polyfill";

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

const buildToken = $("#build-token").data("token");
let apiToken = $("#api-token").data("token");
const buildEndpointUrl = new URL("build", BASE_URL);
const image = new BinderRepository(
providerSpec,
buildEndpointUrl,
buildToken,
false,
{ apiToken },
);

for await (const data of image.fetch()) {
Expand Down
1 change: 1 addition & 0 deletions binderhub/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
{% block head %}
<meta id="base-url" data-url="{{base_url}}">
<meta id="badge-base-url" data-url="{{badge_base_url}}">
<meta id="api-token" data-token="{{ api_token }}">
<script src="{{static_url("dist/bundle.js")}}"></script>
{{ super() }}
{% endblock head %}
Expand Down
1 change: 1 addition & 0 deletions binderhub/templates/loading.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<meta id="base-url" data-url="{{base_url}}">
<meta id="badge-base-url" data-url="{{badge_base_url}}">
<meta id="build-token" data-token="{{ build_token }}">
<meta id="api-token" data-token="{{ api_token }}">
{{ super() }}
<script src="{{static_url("dist/bundle.js")}}"></script>
<link href="{{static_url("loading.css")}}" rel="stylesheet">
Expand Down
79 changes: 55 additions & 24 deletions js/packages/binderhub-client/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { NativeEventSource, EventSourcePolyfill } from "event-source-polyfill";
import { fetchEventSource } from "@microsoft/fetch-event-source";
import { EventIterator } from "event-iterator";

// Use native browser EventSource if available, and use the polyfill if not available
const EventSource = NativeEventSource || EventSourcePolyfill;

function _getXSRFToken() {
// from @jupyterlab/services
let cookie = "";
try {
cookie = document.cookie;
} catch (e) {
// e.g. SecurityError in case of CSP Sandbox
return null;
}
const xsrfTokenMatch = cookie.match("\\b_xsrf=([^;]*)\\b");
if (xsrfTokenMatch) {
return xsrfTokenMatch[1];
}
return null;
}
/**
* Build (and optionally launch) a repository by talking to a BinderHub API endpoint
*/
Expand All @@ -14,8 +26,15 @@ export class BinderRepository {
* @param {URL} buildEndpointUrl API URL of the build endpoint to talk to
* @param {string} [buildToken] Optional JWT based build token if this binderhub installation requires using build tokens
* @param {boolean} [buildOnly] Opt out of launching built image by default by passing `build_only` param
* @param {string} [apiToken] Optional Bearer token for authenticating requests
*/
constructor(providerSpec, buildEndpointUrl, buildToken, buildOnly) {
constructor(
providerSpec,
buildEndpointUrl,
buildToken,
buildOnly,
{ apiToken },
) {
this.providerSpec = providerSpec;
// Make sure that buildEndpointUrl is a real URL - this ensures hostname is properly set
if (!(buildEndpointUrl instanceof URL)) {
Expand All @@ -40,6 +59,7 @@ export class BinderRepository {
if (buildOnly) {
this.buildUrl.searchParams.append("build_only", "true");
}
this.apiToken = apiToken;

this.eventIteratorQueue = null;
}
Expand Down Expand Up @@ -67,26 +87,37 @@ export class BinderRepository {
* @returns {AsyncIterable<Line>} An async iterator yielding responses from the API as they come in
*/
fetch() {
this.eventSource = new EventSource(this.buildUrl);
return new EventIterator((queue) => {
const headers = {};
if (this.apiToken && this.apiToken.length > 0) {
headers["Authorization"] = `Bearer ${this.apiToken}`;
} else {
const xsrf = _getXSRFToken();
if (xsrf) {
headers["X-Xsrftoken"] = xsrf;
}
}
return new EventIterator(async (queue) => {
this.eventIteratorQueue = queue;
this.eventSource.onerror = () => {
queue.push({
phase: "failed",
message: "Failed to connect to event stream\n",
});
queue.stop();
};

this.eventSource.addEventListener("message", (event) => {
// console.log("message received")
// console.log(event)
const data = JSON.parse(event.data);
// FIXME: fix case of phase/state upstream
if (data.phase) {
data.phase = data.phase.toLowerCase();
}
queue.push(data);
await fetchEventSource(this.buildUrl, {
headers,
onerror: () => {
queue.push({
phase: "failed",
message: "Failed to connect to event stream\n",
});
queue.stop();
},

onmessage: (event) => {
// console.log("message received")
// console.log(event)
const data = JSON.parse(event.data);
// FIXME: fix case of phase/state upstream
if (data.phase) {
data.phase = data.phase.toLowerCase();
}
queue.push(data);
},
});
});
}
Expand Down
2 changes: 1 addition & 1 deletion js/packages/binderhub-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"homepage": "https://github.com/jupyterhub/binderhub#readme",
"dependencies": {
"event-source-polyfill": "^1.0.31",
"@microsoft/fetch-event-source": "^2.0.1",
"event-iterator": "^2.0.0"
}
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"dependencies": {
"bootstrap": "^3.4.1",
"clipboard": "^2.0.11",
"event-source-polyfill": "^1.0.31",
"jquery": "^3.6.4",
"xterm": "^5.1.0",
"xterm-addon-fit": "^0.7.0"
Expand Down

0 comments on commit 8c1bae0

Please sign in to comment.