Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle api tokens and xsrf #1847

Merged
merged 12 commits into from
May 14, 2024
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");
minrk marked this conversation as resolved.
Show resolved Hide resolved
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regexes in anything security sensitive always make me nervous haha. Can you either add a comment here explaining what's going on, or a direct link to the specific place from @jupyterlab/services this was stolen from?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added link and explanation. It's still astonishing to me that there is no API for geting cookies by name in browsers, but it is {name}={anything but a semicolon}[; again]

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 },
) {
minrk marked this conversation as resolved.
Show resolved Hide resolved
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